ReactNative作为移动端的框架,首要的功能就是提供Native的界面性能和能够使用各种原生特性的API,今天就来分享一下无线iOS在应用ReactNative实际开发过程中的经验和碰到的问题。
在App的实际开发过程中,会有各种个性化的需求和业务逻辑,这时候ReactNative原生的API并不能满足个性化的需求。于是,封装个性化的接口就成为了我们的刚需。值得高兴的是ReactNative让这种封装变的非常简单和舒适,接下来以一个简单的接口为例。
首先定义一个CustomAlert类,然后实现RCTBridgeModule协议。
@interface CustomAlert : NSObject <RCTBridgeModule>
接下来在类的实现中添加ReactNative的RCT_EXPORT_MODULE()宏,ReactNative框架在加载页面的时候就会为CustomAlert生成一个对应的RCTModuleData。ReactNative的桥会在CustomAlert 的+load方法执行的时候将类名添加到RCTGetModuleClasses数组中并生成RCTModuleData(包含了对JS暴露的方法等信息)。
RCT_EXPORT_MODULE()决定了那些类会暴露给JS,RCT_EXPORT_METHOD()则标识了哪些方法可以提供给JS,RCT_EXPORT_METHOD会在要暴露的接口名称前加上前缀@"__rct_export__",然后当生成RCTModuleData时通过循环遍历类的selector找出带这个前缀的seletor加入到methods数组中。
那么现在来实现一个简单的alert接口供JS调用:
RCT_EXPORT_METHOD(showAlertWithMessage:(NSString*)message buttontitle:(NSString*)title){
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"RNTest" message:message delegate:nil cancelButtonTitle:title otherButtonTitles:nil];
到这里一个简单的API接口就定好了,那么JS要怎么使用呢?也是非常简单,只需要在代码最开始 import NativeModules,然后就可以在JS中调用:
NativeModules.CustomAlert.showAlertWithMessage('这是一个测试','OK');
一个简单的API模块就封装完成了。如果想偷懒的话,也是可以直接将工程中已有的类和函数按照上面的步骤很快就可以封装成ReactNative的模块。RCT_EXPORT_MODULE()在一个类中只需要实现一次。不管这个函数名称定义的多么复杂ReactNative只会截取第一段作(到第一个冒号为止)为对外暴露的函数名,所以ReactNative提供了另一个宏RCT_REMAP_METHOD。这个宏允许你给函数定义一个别名,这样可以更好的区分那些第一段相同的函数。例如下面显示alert的两个函数,差别在于后面的buttontitle一个是可配的一个是固定的,但是由于第一段一样,所以需要用RCT_REMAP_METHOD进行区分。
RCT_REMAP_METHOD(showAlertWithMessage,showAlertWithMessage:(NSString*)message){
RCT_REMAP_METHOD(showAlertWithMessageAndButtonTitle,showAlertWithMessage:(NSString*)message buttontitle:(NSString*)title){
在发现封装一个模块这么easy了之后我们已经迫不及待的要试试了,于是乎潇洒的commoand+R。本以为应该是一个大大alert出现我们面前,却发现什么都没有。但是在控制台却看到了长长的log:
This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes.
再熟悉不过的log,在子线程更新了UI,也就是说OC提供给JS的调用默认都是发生在子线程里的,而iOS的UI更新操作要放在主线程中。大部分开发者在子线程里想要操作UI时都是甩一个dispatch_get_main_queue让UI操作在里面玩儿,这个地方也确实解决了UI的问题,在加上强制主线程操作后我们的Alert就出来了。但是如果一个模块有很多API都需要UI操作呢,岂不是dispatch_get_main_queue满天飞?好在ReactNative提供了一个方法来避免这种情况:在模块中实现methodQueue方法,返回一个线程,该模块的暴露的接口在调用的时候都在该线程下执行。例如:
- (dispatch_queue_t)methodQueue{
return dispatch_get_main_queue();
ReactNative引擎在创建模块类的时候会询问时候实现了methodQueue方法,如果实现了则保存该线程,在后面发生调用的时候,确保调用发生在设定的线程上,如果没有则会开一个以模块名命名的子线程(部分注释和log已删掉)。
if (_instance && !_methodQueue && _bridge.valid) {
BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)];
if (implementsMethodQueue && _bridge.valid) {
_methodQueue = _instance.methodQueue;
if (!_methodQueue && _bridge.valid) {
_queueName = [NSString stringWithFormat:@"com.facebook.react.%@Queue", self.name];
_methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
if (implementsMethodQueue) {
[(id)_instance setValue:_methodQueue forKey:@"methodQueue"];
@catch (NSException *exception) {
通关这段函数也可以看出RN尽可能的进行异步操作。这样避免了因为RN引擎导致主线程的阻塞。实现了methodQueue之后仍然可以在接口内使用多线程进行操作,methodQueue只保证接口的调用线程。在弄懂了RN对线程的处理之后就简单多了,我们也见到了心仪已久的alert:
JS在调用完模块后总想做点什么,所以RN提供了一个方法来满足JS的合理需求。RN通过一个特殊的参数来实现回调并将模块执行后的一些结果返回给JS,虽然facebook在文档中之处回调还是实验阶段,但是在实际用的过程中还是很稳定的。RN中前后端数据交互形式目前来看有三种。
RN通过一个特殊的参数来支持回调JS的函数,这个参数就是RCTResponseSenderBlock。在定义模块接口的时候加上这个参数
RCT_EXPORT_METHOD(showAlertWithMessage:(NSString*)message buttontitle:(NSString*)title callback:(RCTResponseSenderBlock)callback)
NativeModules.CustomAlert.showAlertWithMessage('这是一个测试','OK',this.alertCallBack);
然后在native处理完一系列业务逻辑后再执行block就能够达到回调JS代码。但是这里有两个地方需要注意:
一个函数中只能注册一个RCTResponseSenderBlock。
这两个特性就决定了回调不能用来做持续的交互,对应到实际开发过程中,例如语音控件需要录制的同时显示出内容,这个时候用回调函数返回结果就会导致崩溃。
上面也提到了RN有很重要的一点就是异步,但是在开发过程中不可避免的需要在得到某些数据之后再进行业务代码。RN提供了Promises来满足这种需求。native端的写法类似上面回调函数,JS则通过async+await来等待返回值。
async function getData() {
var values = await CustomData.obtainData();
Alert.alert(values.name);
回到上面那个语音录制的问题,既然回调函数不能解决问题那么怎么来处理这个不停的需要传递数据的情况呢?ReactNative提供的一种方法是Native端主动发送事件给JS端,在事件发送的时候将数据也带过去。
[self.bridge.eventDispatcher sendDeviceEventWithName:@"VoiceRecordEvent" body:result];
JS端只要在调用之前监听这个事件就能持续的接收Native传过来的数据。
var voiceSubscription = DeviceEventEmitter.addListener('VoiceRecordEvent', (voiceResult) => this.setVoiceRecordResult(voiceResult))
voiceSubscription.remove();
上面就是ReactNative中模块开发的主要流程和注意事项,接下来分享一个在开发中遇到的兼容性的一个诡异错误,在进行系统适配的时候发现同样的代码在iOS8和iOS9上都没有问题,但是在iOS7的机型上就一直报错,而且还很诡异,见下图:
在网上找了很久也没有结论,跟踪了js代码的解析过程也没有很好的错误提示,最后采用了简单粗暴的方法来一点点缩小错误代码范围。通过注释掉require文件将错误定位在某个js文件中,最后发现是因为有重名的函数(多人开发的时候在一个文件中添加了同样的生命周期的函数)导致解析失败,我们猜测可能是因为iOS7系统上的JavaScriptCore对函数重复的处理还不太完善。
在折腾的过程中发现RN的两个导出函数的宏RCT_EXPORT_METHOD和RCT_EXTERN_METHOD最终是调用同一个宏。于是尝试用RCT_EXTERN_METHOD来导出接口,并且意外的发现这种写法更简练,看起来更舒服。
RCT_EXTERN_METHOD(showAlertWithMessage:(NSString*)message buttontitle:(NSString *)title);
- (void)showAlertWithMessage:(NSString*)message buttontitle:(NSString *)title{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"RNTest" message:message delegate:nil cancelButtonTitle:title otherButtonTitles:nil];
在实现了自定义ReactNative的模块之后也可以发现ReactNative在api调用上的思想:能异步的尽量都异步,不干扰主线程UI操作,UI类的API提供运行在主线程的方式,尽可能的对原有代码减少入侵。因为是实战篇所以很多细节和实现原理并没有太深入,在接下来的系列文章中将带来ReactNative Components的实战,并分享更多原理方面的细节。感谢您的阅读。
506500ReactNative iOS自定义模块实战
文章评论