React Native(RN)与原生代码之间的通信是混合开发中的关键部分。本文将详细阐述原生与RN页面相互跳转、方法间的相互调用、以及H5页面调用原生页面进而调用RN页面等通信机制。
一、原生与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容器。通过类型为 NSDictionary 的 initialProperties 可以将任意属性传递给RN应用,这些属性会在RN内部转化为JSON对象。
首先创建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;
@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
}
然后创建容纳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,实现该协议的类会自动注册到Bridge中。所有实现 RCTBridgeModule 的类都必须包括宏 RCT_EXPORT_MODULE(),用于自动注册Module。
首先新建类实现 RCTBridgeModule 协议。在.h文件中:
@interface xxxModule : NSObject<RCTBridgeModule>
@end
在.m文件中导出方法:
RCT_EXPORT_METHOD(goBack){
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"configBack" object:nil];
});
}
在承载RN页面的控制器中接收通知,并实现从RN返回到原生页面的方法:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(navagateBack) name:@"configBack" object:nil];
- (void)navagateBack{
[self.navigationController popViewControllerAnimated:YES];
}
在RN界面中,通过 NativeModules 引入原生的module类,并调用返回原生界面的方法:
import {
NativeModules,
} from 'react-native';
onPressBack={() => {
NativeModules.xxxModule.goBack();
}}
3. 将原生参数传递给RN
通过 RCT_EXPORT_METHOD 可以将原生的参数传递给RN,或让RN实现原生的操作。例如,在原生端定义方法:
// 获取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 时传递不同路径参数实现。在需要跳转的类中传递字段:
XXXReactHomeViewController *reactVC = [[XXXReactHomeViewController alloc]init];
reactVC.rnPath = @"SugarStack";
[self.navigationController pushViewController:reactVC animated:YES];
在RN端接收属性并跳转页面。使用 react-navigation 导航栏控制器,通过从原生接收的参数 path 判断要显示的屏幕:
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
定义栈并放入导航中:
const SugarStack = createStackNavigator({
SugarFriend,
SugarFriendDetail,
RosterSearch,
});
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页面
这需要使用深度链接(deep linking)技术。首先在RN配置导航容器,使其能够从传入的URI中提取路径:
const SimpleApp = createAppContainer(createStackNavigator({...}));
const prefix = 'mychat://';
const MainApp = () => <SimpleApp uriPrefix={prefix} />;
在AppDelegate文件中配置iOS应用程序处理该URI方案:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:app openURL:url options:options];
}
在Xcode中设置 info -> URL Type 为mychat,以启用深度链接。
二、打包
- 导出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/
-
将资源包导入到iOS项目:将生成的
assets和main.jsbundle文件拖入iOS工程,勾选适当的选项。 -
打包发布:通过Xcode的
Product->Archive打ipa包。
三、调试中遇见的一点小问题
在iOS真机调试时,可能遇到连接超时或无法加载js server的问题。确保mac和手机连接同一网络后,检查xCode中是否有 域名.xip.io 文件。如果没有,可能是因为项目从别处拷贝,导致ip.txt文件缺失。解决方法是在 guessPackagerHost 方法中直接返回本机地址,而不是localhost。
另外,在RN环境6.0以上和React-navigation 4.x中,可能出现 null is not an object(evaluating '_RNGestureHandlerModule.default.Direction') 错误。确保Podfile文件中包含以下库:
pod 'RNGestureHandler', :podspec => '../node_modules/react-native-gesture-handler/RNGestureHandler.podspec'
通过以上步骤,可以有效地实现原生与React Native之间的通信,并解决常见的打包和调试问题。