React Native与原生通信(iOS端)

emmm…… 先说个题外话,时隔一年,再遇RN,较之以前唯一不同的一点就是遇到的坑终于有人先踩了😂本文会通过原生与RN页面相互跳转、方法间的相互调用、以及H5页面调用原生页面进而调用RN页面等方面来阐述原生与RN间的通信。不要疑惑为啥子会有这种撒娇三连的操作,我也只能摊手道:存在即合理(无奈╮(╯▽╰)╭.gif)。


一、原生与RN通信

先做点准备工作叭~ 通过react-native init创建一个RN的新项目,此后将会得到一个内部带有iosandroid目录的文件夹。把这两个目录下的文件换成自己的项目。位置如下图所示。

修改podfile文件,将RN需要的库引入到自己的项目中。

pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
  pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
  pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
  pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
  pod 'React', :path => '../node_modules/react-native/'
  pod 'React-Core', :path => '../node_modules/react-native/'
  pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
  pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
  pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
  pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
  pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
  pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
  pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
  pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
  pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
  pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
  pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
  pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'

  pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
  pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
  pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
  pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
  pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon"
  pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
  pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'

  pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
  pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
  pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

1、 原生跳RN页面

RCTRootView是一个可以将RN视图封装到原生组件中并且提供联通原生和被托管端接口的UIView容器。properties属性用于在React中将信息从父组件传递给子组件。
RCTRootView在初始化函数之时,通过类型为NSDictionaryinitialProperties可以将任意属性传递给RN应用。这一字典参数会在RN内部被转化为可供组件调用的JSON对象。

1) 创建RN的桥接管理类(单例)实现RCTBridgeDelegate协议

// .h文件
#import <React/RCTBridgeModule.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTBridgeDelegate.h>


@interface XXXRCTManager : NSObject<RCTBridgeDelegate>
+ (instancetype)shareInstance;

// 全局唯一的bridge
@property (nonatomic, readonly, strong) RCTBridge *bridge;
@end
//.m文件
static XXXRCTManager *_instance = nil;
+ (instancetype)shareInstance{
    if (_instance == nil) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    if (_instance == nil) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [super allocWithZone:zone];
        });
    }
    return _instance;
}

-(instancetype)init{
    if (self = [super init]) {
        _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
    }
    return self;
}

实现sourceURLForBridge方法。调试模式下,读取index文件资源,打包则读取jsbundle中的资源。

#pragma mark - RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
# if DEBUG
        return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"
                                                              fallbackResource:nil];
# else
    return [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"jsbundle"];
#endif
}

2) 创建容纳RN页面的控制器

//.h
@interface XXXReactHomeViewController : UIViewController
@property(nonatomic,strong)NSString *rnPath; // 传递给RN的数据 页面名称

@end

在.m文件中初始化RCTRootView,并将其添加到控制器页面上

 NSDictionary *props = @{@"path" : self.rnPath};
 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[SZLRCTManager shareInstance].bridge                                                     moduleName:@"RN中AppRegistry注册的名字"                                              initialProperties:props];

如此一来,iOS页面就能跳转到RN项目的首页了。轻松加愉快啊。

2、 RN页面跳原生页面及调用原生方法

RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到iOS代码中对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信,如此就能实现RN与iOS原生的相互调用。
需要注意的是:所有实现RCTBridgeModule的类都必须包括这条宏:RCT_EXPORT_MODULE()。它的作用是自动注册一个Module,当原生的桥加载之时,这个Module可以在JavaScript Bridge中调用。
先来看一下它的定义:

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

由此可以看出RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Module的名称。

1)新建类实现RCTBridgeModule协议

// .h
@interface xxxModule : NSObject<RCTBridgeModule>
@end
//.m
RCT_EXPORT_METHOD(goBack){
    //    用通知的方式返回原生页面
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"configBack" object:nil];
    });   
}
  1. 在XXXReactHomeViewController即承载RN页面的控制器中,接收通知,并实现从RN返回到原生页面的方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(navagateBack) name:@"configBack" object:nil];
- (void)navagateBack{
    [self.navigationController popViewControllerAnimated:YES];
}

3)在RN的界面中,通过NativeModules引入原生的module类,并调用返回原生界面的方法。

import {
  NativeModules,
} from 'react-native';
 onPressBack={() => {
        NativeModules.xxxModule.goBack();
        }}

以上骚操作已经可以满足RN跳转到原生界面的需求了。
however,在实际项目中,这还远远不够。比如说me正在进行的项目,需要将登录获取到的token传递给RN界面,一旦失效,则立即唤起原生的登录页面。

咳咳,好累ヽ( ̄▽ ̄)و坐直了。

…………………………………………假装我是分割线……………………………………

3、将原生参数传递给RN

将原生的参数传递给RN,或是让RN实现原生的某些操作可以通过RCT_EXPORT_METHOD实现。它是用来定义被JavaScript调用的方法的宏。RCT_EXTERN_METHOD调用了宏RCT_EXTERN_REMAP_METHOD。下面是该宏的定义:

#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
  + (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
    RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
    return @[@#js_name, @#method]; \
  }

由此可以看出,它的作用是在RCT_EXPORT_MODULE定义的Module下面,定义一个可以被JavaScript调用的方法。
RCT_EXPORT_MODULE的使用,需要写入方法名,参数以及完整的实现。

  1. 原生定义方法
// 获取token
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getToken)
{
    NSString *token = [[NSUserDefaults standardUserDefaults]objectForKey:@"token"];
    return token;
}
// 退出登录
RCT_EXPORT_METHOD(signOut){
    dispatch_async(dispatch_get_main_queue(), ^{
        AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        LoginViewController *loginVC = [[LoginViewController alloc]init];
        UINavigationController *nav = [[UINavigationController alloc]initWithRootViewController:loginVC];
        appDelegate.window.rootViewController = nav;
    });
    
}
  1. RN方调用
import { NativeModules } from 'react-native';
// 拿token
  requestObj.headers.Authorization = NativeModules.config.getToken();
  // 调用原生的退出登录方法
   NativeModules.XXXModule.signOut();

4、 多入口跳转到RN不同的页面

项目中有这样一个需求,要从不同的原生页面进入到不同的RN页面。此时,单纯通过导航跳转就无法解决该问题了。


在初始化RCTRootView之时,通过initWithBridge:(RCTBridge *)bridge方法将要展示的页面路径通过属性传递给RN。RN方接收到信息,再根据传入的路径决定要跳转到哪个页面。
1) 原生端传入数据
创建RCTRootView的代码在上文中已给出。在需要跳转的类中,传递字段。

 XXXReactHomeViewController *reactVC = [[XXXReactHomeViewController alloc]init];
            reactVC.rnPath = @"SugarStack";
            [self.navigationController pushViewController:reactVC animated:YES];
  1. RN端接收属性并跳转页面
    在本项目中,采用的是react-navigation导航栏控制器。
    飞机票👻:react-navigation
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

每个栈中都存放不同页面。如:

const SugarStack = createStackNavigator({
  SugarFriend,
  SugarFriendDetail,
  RosterSearch,
});

将栈放入到导航中去,一次只显示一个屏幕。通过从原生接收的参数path来判断要显示哪个屏幕。

const App = function (props) {
  const AppNavigator = createSwitchNavigator(
    {
      AppStack,
      SugarStack,
    },
    {
      initialRouteName: props.path || 'AppStack',
    },
  );
  const Navigation = createAppContainer(AppNavigator);
  return (
    <Provider store={store}>
      <StatusBar translucent backgroundColor="#00000000" barStyle="dark-content" showHideTransition="Slide" />

      <Navigation />
    </Provider>
  );
};

5、 H5页面调用原生页面进而调用RN页面(吐血三连)

这波骚操作源于项目本身就是一个H5与原生混合的app,其中有一个酱紫的功能。H5页显示一条消息提醒用户有待办事项,而用户点击进行处理的操作是需要跳转到RN页面的。如果按照前文中带参跳转也只能跳转到RN栈的第一个页面。因此需要使用到deep-link方案。深度链接是一项可以让一个App通过一个URL地址打开,之后导航至特定页面或者资源,或者展示特定UI的技术
传送门👻:Deep linking

1)RN配置导航容器,使其能够从传入应用程序的 URI 中提取路径。

const SimpleApp = createAppContainer(createStackNavigator({...}));
const prefix = 'mychat://';
const MainApp = () => <SimpleApp uriPrefix={prefix} />;

2)在Appdelegate文件中,将iOS应用程序配置为使用 mychat:// URI 方案打开。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:app openURL:url options:options];
}

3)在xcode中,设置info->URL Type为mychat

二、打包

1) 导出js bundle包和图片资源
终端进入RN项目的根目录下创建文件夹,此处名为release_ios

react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

entry-file代表入口文件,platform是平台的意思,后面一串是指输出资源到哪个文件或文件夹。

2) 将资源包导入到iOS项目。
通过上述命令,可以在relise_ios文件夹下找到assetsmain.jsbundle。将这两个文件拖入到iOS工程下。勾选第一和第三选项

3) 打包发布
xCode->Product->Archive打ipa包

三、调试中遇见的一点小问题

iOS真机调试,reload的时候永远没反应,摇一摇弹出的调试界面也差了好几个按钮。把上文中所打的main.jsbundle移除后,真机运行直接奔溃。真真是一入红屏深似海:

Connection to http://localhost:8081/debugger-proxy?role=client timed out. Are you running node proxy? If you are running on the device, check if you have the right IP address in RCTWebSocketExecutor.m.

AFN弹出提示:“未能找到使用指定主机名的服务器”。也就是说RN并未调起js server。
确保mac和手机连的是同一网络之后,去xCode中搜索域名.xip.io。发现并没有这个文件。

在受到这两篇文章的启发之后,才明白
传送门👻:
在设备上运行
iOS 真机 No bundle URL present

我的iOS项目是从别处拷贝过来,而ip.txt文件是在没有设置SKIP_BUNDLING的情况下初次构建的时候创建的。在构建app之后,加入做了clean操作或者拷贝到其他机器,创建ip.txt的步骤就被省略了。
解决方法是:到guessPackagerHost方法中,不要返回localhost,直接返回本机地址即可。

关于null is not an object(evaluating '_RNGestureHandlerModule.default.Direction')

RN环境在6.0以上,React-navigation在4.x。重装过pod或者node module还是无济于事。遂在想是不是没有在podfile文件中加入。之后查询到该信息。

pod 'RNGestureHandler', :podspec => '../node_modules/react-native-gesture-handler/RNGestureHandler.podspec'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容