React Native混合开发(iOS)下的数据交互
3.2 初始化RCTRootView的数据传递
上文提到在 RCTRootView 初始化的时候可以进行参数的传递,那么参数是如何被接收处理的呢?下面直接看代码:
NSDictionary *param = @{@"scores" :@[
@{@"name" : @"Alex",@"value": @"42"},
@{@"name" : @"Joel",@"value": @"10"},
@{@"name" : @"Zona",@"value": @"20"}
]
};
NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"ParamPassCp"
initialProperties:param
launchOptions:nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];
相比于上面 Hello World 的例子,这里初始化了一个字典,存储了一些名字及对应的分数,并在 RCTRootView 初始化的时候作为 initialProperties 的参数进行传递。
在 JS 端是如何接收的呢?
class ParamPassCp extends React.Component {
render() {
var contents = this.props["scores"].map(
score => <Text key={score.name}>{score.name}:{score.value}{"\n"}</Text>
);
return (
<View style={styles.container}>
<Text style={styles.highScoresTitle}>
{contents}
</Text>
</View>
);
}
}
同样是在 render() 方法中,我们直接从 props 参数中读取字段的 key 获取对应的数据 Array,并通过 map 方法将其每一个数据单项映射成显示数据的标签,最后将标签列表置于View中返回。其中,对于变量 contents,我们需要用 {} 将其嵌入到 JSX 语句中。
props 即是 React 组件的属性,是一种父级向子级传递数据的方式。上面读取属性的代码也可以写成:this.props.scores。显然,通过 initialProperties 传递过来的字典变成了 React 组件的属性,可直接读取使用。但是 props 对于组件本身来说是不可变的,只能经由父组件传递更新。
我们还设置了 view 的 style,这里将 style 整体定义成变量初始后传递给view,借以保持代码的清晰整洁。
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
highScoresTitle: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
运行结果如下:
除了在初始化 RCTRootView 的时候可以传递参数,OC还可以用更新的方式传递数据给 JS 组件,修改这个属性,JS端会调用相应的渲染方法。
_rootView.appProperties = @{@"scores" :@[
@{@"name" : @"Alex",@"value": @"42"},
@{@"name" : @"Joel",@"value": @"10"},
@{@"name" : @"Zona",@"value": [NSString stringWithFormat:@"%ld",(long)_score++]}
]
};
这两种传递数据的方式是 OC 向 JS 传递数据的主要方式。
3.3 RN调用原生方法
RN向OC传递数据的主要形式之一便是通过在调用原生方法的时候传递参数。再而也为了让React Native可以利用现有原生庞大的组件资源,React Native在设计之初就考虑到了让React Native可以方便的调用Native端的方法。
3.3.1 支持调用的步骤
要想让iOS类内的方法能够被RN调用,类比RN端的组件注册,iOS端同样需要注册该类。首先便需要原生类实现协议:RCTBridgeModule,实现该协议的类,会自动注册到Object-C对应的Bridge中。所以定义可以让RN调用的类可以这样写
#import "RCTBridgeModule.h"
@interface RNIOSLog : NSObject<RCTBridgeModule>
@end
所有实现 RCTBridgeModule 的类都必须显示的使用宏命令:
@implementation RNIOSLog
RCT_EXPORT_MODULE();
@end
该宏的作用是:自动为该类注册为JS端的模块,当Object-c Bridge加载的时候。这个类注册的模块可以被JavaScript Bridge调用。当然该宏可以接受一个参数作为注册的模块名,默认值是该类的名称。
注册完模块之后,还需要注册模块下需要暴露给JS的方法。此外,暴露出的方法返回值必须为void。
RCT_EXPORT_METHOD(show:(NSString *)msg){
NSLog(@"msg:%@",msg);
}
原生的模块方法注册好之后,JS端该如何引用该类呢?
import {NativeModules} from "react-native";
var RNIOSLog = NativeModules.RNIOSLog;
引入到JS模块下之后,便可直接调用。
class RNLogCp extends Component {
render() {
return (
<View style={styles.container}>
<TouchableHighlight onPress={()=>RNIOSLog.show('from react native')}
style={styles.btn}>
<Text>showLog</Text>
</TouchableHighlight>
</View>
);
}
}
在RN中,TouchableXXX就表示是按钮控件。TouchableHighlight在点击的时候,该控件会高亮显示。此外还有TouchableOpacity,TouchableNativeFeedback 和TouchableWithoutFeedback。
到这一步之后,便是让 RN 页面展示出来,点击 RN 组件上的按钮便可看到 RN 调用 OC 的效果。同样的,我们初始化 RCTRootView 并设置为新页面的根view,并push出来显示。
NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"RNLogCp"
initialProperties:nil
launchOptions:nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];
3.3.2 RN调用OC的回调
对于OC暴露给RN的方法,要求不能有返回值。但是在很多应用场景下,我们也需要对调用之后的返回值进行相应的处理,这样就需要使用回调方法来对结果进行处理。在RN中专门定义了一个用于回调的参数 RCTReponseSenderBlock。
typedef void (^RCTResponseSenderBlock)(NSArray *response);
它接收了一个叫做 response 的 NSArray 的参数,其中 response[0] 代表着错误信息error,如果没有错误则传入null,即[NSNull null],后面的参数传入自定义的内容。
RCT_EXPORT_METHOD(showWithCallback:(RCTResponseSenderBlock)callback){
//do something you want
//callback(@"error",@"something is wrong");
callback(@[[NSNull null],@"call back from native"]);
}
在RN中,是这样调用Native方法并处理回调的:
_logCallback() {
RNIOSLog.showWithCallback(function (err, data){
if (err) {
console.warn(err, data);
} else {
console.warn(data,'无错回调');
}
});
}
<TouchableHighlight onPress={()=>this._logCallback()}>
<Text>showLogCallback</Text>
</TouchableHighlight>
之后便是同样的 RN 页面展示方法,初始化 RCTRootView 并设置为新页面的根view,并push出来显示。运行之后我们每次点击 RN 页面上的按钮标签都能看到RN调用Native端的回调log,运行效果如下图:
3.3.3 RN调用OC时的线程问题
JavaScript 代码都是单线程运行的,而调用到Native模块时都是默认运行在各自独立的线程上,所以可知RN调用Native的时候都是异步的。因此若是调用的Native方法有需要操作UI的,必须指定在主线程中运行,否则会出现一些莫名其妙的问题。比如RN调用的Native方法里需要弹出原生的 UIAlertView ,则可以在操作 UIAlertView 的时候用 GCD 切换到主线程:
dispatch_async(dispatch_get_main_queue(), ^{
//操作UI
});
此外,如果需要对整个导出的类都指定到某个特定的线程中去运行,那么在每个导出的方法里用 GCD 的方式去切换线程会显得很繁琐,则可以在类中实现 methodQueue 方法:
- (dispatch_queue_t)methodQueue{
return dispatch_get_main_queue();
}
只要实现了该方法并返回了特定的线程,那么该类下所有的方法在被RN调用时都会自觉的运行在该方法指定的线程下。
3.3.4 bridge资源问题
对于 RCTRootView 官方提供了两种初始化方式
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties;
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions;
对于第二种创建方式(initWithBundleURL),其会在每次调用时在方法内部创建一个 RCTBridge,且多个不同 RCTRootView 并不能共享 RCTBridge,这比较耗费时间和资源。因此对于一个半RN半native的应用的应用来说,最好还是使用第一种方式(initWithBridge)初始化 RCTRootView。
对于 initWithBridge 的方式初始化 RCTRootView,首先需要初始化一个 RCTBridge并保存,以便在需要的时候使用。在此之前,类本身需要实现 RCTBridgeDelegate 协议,
@interface ViewController ()<RCTBridgeDelegate>
@property (nonatomic, strong) RCTBridge *bridge;
@end
@implementation ViewController
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
return [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
}
@end
在协议方法 sourceURLForBridge 中,返回 RN 模块地址。然后便可以初始化我们的bridge,
//使用保留的 RCTBridge 初始化 RCTRootView 更节省资源,不用每次初始化bridge
_bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
最后便可以到处使用该 bridge 初始化 RCTRootView了,这样能有效的节省每次初始化 bridge 的时间和资源耗费。
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge
moduleName:@"HelloWorldCp"
initialProperties:nil];
3.4 原生调用RN方法
现在,我们已经知道了在RN中该怎么直接调用OC中的方法,那么OC该如何主动的去调用 RN方法呢?
在以前的RN版本中,可以使用 sendDeviceEventWithName:body: 的方式来将调用请求发送到JS端,JS端用 addListener 的方式监听对应的关键字并实现方法即可实现OC调用RN方法。但是随着RN版本的更新,当继续使用这种互动方式的时候,在xcode下会出现警告:
<font color=#DC143C>'sendDeviceEventWithName:body:' is deprecated: Subclass RCTEventEmitter instead</font>
适应新的Api调用方式,让我们开始用起 RCTEventEmitter 来,其基本对接步骤是一致的。我们可以定义一个专门用来调用RN方法的类,在不影响其他原生模块的条件下方便和RN端对接。
-
1.该类需要继承自 RCTEventEmitter ,并且需要向RN端那边导出自己:
#import "RCTEventEmitter.h" @interface CallRNTest : RCTEventEmitter<RCTBridgeModule> @end
-
2.然后在 .m 文件中,在子类中为父类 RCTEventEmitter 的 bridge 生成 set/get方法,并使用用于导出模块的宏。
@implementation CallRNTest @synthesize bridge = _bridge; RCT_EXPORT_MODULE(); @end
假如不写第二句bridge的代码,在使用时会报没有设置bridge的错误:
<font color=#DC143C>*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'bridge is not set. </font>
-
3.导出所有需要传递的方法的名字
(NSArray<NSString *> *)supportedEvents{ return @[@"callRn"]; }
-
4.你可以在Native端实现在 supportedEvents 中定义的方法的同名方法,便于 区分理解Native端代码,也方便使用者调用。当然你也可以不这么做,反正最终都是使用 sendEventWithName 来进行真正的调用的。
-(void)nativeCallRn:(NSString*)code result:(NSString*) result { [self sendEventWithName:@"callRn" body:@{ @"code": code, @"result": result, }]; }
-
5.在 JS 端导出
import { ... NativeModules, NativeEventEmitter} from 'react-native'; var CallRNTest = NativeModules.CallRNTest; const myNativeEvt = new NativeEventEmitter(CallRNTest);
-
6.在 JS 端绑定
//在组件的生命周期中绑定与解绑 componentWillMount() { //对应原生端的名字 this.listener = myNativeEvt.addListener('callRn', this.callRn.bind(this)); } componentWillUnmount() { this.listener && this.listener.remove(); //记得remove哦 this.listener = null; }
-
7.在 JS 端实现绑定的方法
//接受原生传过来的数据 data={code:,result:} callRn(data) { console.warn(data.code, data.result); }
-
8.在 Native 端合适的时机调用,结束啦~
[self nativeCallRn:@"200" result:@"OC call Rn"];
4.0 Demo Project
写了一个 Demo Project:
https://github.com/xzr123/LittleReactNativeDemo
如果你想试一试运行工程并且还没有安装好 React Native 开发环境,先看这个官方文档配置环境是个不错的选择。
之后,用别忘了启动 RN 本地调试服务器
#cd 到‘node_modules’文件所在目录,然后
npm start
接着用Xcode打开项目工程看看运行效果吧。该Demo是基于 React Native 0.45 版本环境下的。