emmm…… 先说个题外话,时隔一年,再遇RN,较之以前唯一不同的一点就是遇到的坑终于有人先踩了?本文会通过原生与RN页面相互跳转、方法间的相互调用、以及H5页面调用原生页面进而调用RN页面等方面来阐述原生与RN间的通信。不要疑惑为啥子会有这种撒娇三连的操作,我也只能摊手道:存在即合理(无奈╮(╯▽╰)╭.gif)。
一、原生与RN通信
先做点准备工作叭~ 通过
react-native init
创建一个RN的新项目,此后将会得到一个内部带有
ios
和
android
目录的文件夹。把这两个目录下的文件换成自己的项目。位置如下图所示。
修改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
在初始化函数之时,通过类型为
NSDictionary
的
initialProperties
可以将任意属性传递给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];
});
}
复制
- 在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的使用,需要写入方法名,参数以及完整的实现。
- 原生定义方法
// 获取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;
});
}
复制
- 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];
复制
-
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
文件夹下找到
assets
和
main.jsbundle
。将这两个文件拖入到iOS工程下。勾选第一和第三选项
3) 打包发布
xCode
->
Product
->
Archive
打ipa包
三、调试中遇见的一点小问题
iOS真机调试,reload的时候永远没反应,摇一摇弹出的调试界面也差了好几个按钮。把上文中所打的 main.jsbundle
移除后,真机运行直接奔溃。真真是一入红屏深似海:
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'
复制