React Native 原生模块与 JS 模块交互
最近使用 RN 时,需要在 RN 端实现调用系统相册上传选中照片的功能。需要原生模块与 RN 模块进行交互,主要步骤:
- 调用原生系统相;
- 将选取的照片存到本地沙盒;
- 再将沙盒路径传给 RN。
注意: 这里之所以需要将获取的照片存到沙盒下,再将沙盒路径传递给 RN 是因为 RN 端不支持 iOS 中 NSData 二进制流的数据传输格式。而是通过 multipart/form-data 这种表单方式上传文件。
FormData 对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。
在原生与 RN 交互的过程中尝试了几种传递数据的不同方式,这里记录一下,其本质上都是通过实现 RCTBridgeModule 的协议来进行数据交互的。
为了实现 RCTBridgeModule 协议,你的类需要包含 RCT_EXPORT_MODULE() 宏。这个宏也可以添加一个参数用来指定在 Javascript 中访问这个模块的名字。你必须明确的声明要给 Javascript 导出的方法,否则 RN 不会导出任何方法。
按传递数据的不同方式,分为以下三种:
- Callbacks 回调函数;
- 原生模块使用 promise 简化代码;
- RCTEventEmitter — 向 JS 模块发送事件。
Callbacks
提供一个函数把返回值传回给 JavaScript
在原生模块中,
RCT_EXPORT_METHOD(testCallback:(RCTResponseSenderBlock)callback)
{
NSArray *items = @[@"callback ", @"test ", @"array"];
callback(@events);
}
RCTResponseSenderBlock 只接受一个参数 — 传递给 JavaScript 回调函数的参数数组。
在 RN 中调用,
testBack.testCallback((items) => {
this.setState({items: items});
})
原生模块通常只应调用回调函数一次。但是,它可以保存 callback 并在将来调用。如果你想传递一个 Error 类型的对象给 Javascript,可以用 RCTUtils.h提供的 RCTMakeError 函数。
promise
promise 涉及 ES6 的语法,搭配 ES2016(ES7) 标准的async/await语法则效果更佳。具体使用,桥接原生方法的最后两个参数是 RCTPromiseResolveBlock 和RCTPromiseRejectBlock,则对应的 JS 方法就会返回一个 Promise 对象。
RCT_EXPORT_METHOD(getState:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject){
SEPrinterManager *manager = [SEPrinterManager sharedInstance];
CBCentralManager *central = [manager valueForKey:@"centralManager"];
resolve([self successMessage:@{ @"state": @(central.state == CBCentralManagerStatePoweredOn)}]);
}
- (NSDictionary *)successMessage:(id)data{
NSMutableDictionary *response = [NSMutableDictionary dictionary];
response[@"code"] = @(kBluetoohSuccessCode);
response[@"data"] = data;
return response;
}
现在 JavaScript 端的方法会返回一个 Promise。
import { NativeModules } from 'react-native';
class ConnectBLEPrinter extends Component {
NativeModules.MCRNBlueTooth.getState.then(res => {
console.log('BLE state:' + res. state);
}).catch((error) => {
});
}
通过 Callbacks 和通过 Promises 的方式,都可以向 JS 模块传递数据,但都是只能传递一次。如果需要多次向 JS 模块传递数据,可以尝试 RCTEventEmitter 的方式。
RCTEventEmitter
发送事件的方法 sendEvent 定义在 原生 module 类中,在需要发送事件的地方调用 sendEvent 方法就可以将事件通知发给 RN 端。类似于 iOS 中的通知方法的是实现。 iOS 端与 Android 不同,不使用 DeviceEventEmitter 做监听,而是用 NativeEventEmitter。具体实现方案如下:
自定义的模块类头文件要继承自 RCTEventEmitter,
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>
NS_ASSUME_NONNULL_BEGIN
@interface MJTBlueToothManager : RCTEventEmitter <RCTBridgeModule>
@end
NS_ASSUME_NONNULL_END
声明事件,
RCT_EXPORT_METHOD(scanBLEList){
SEPrinterManager *manager = [SEPrinterManager sharedInstance];
[manager startScanPerpheralTimeout:10 Success:^(NSArray<CBPeripheral *> *perpherals,BOOL isTimeout) {
NSLog(@"Test-----------");
self.deviceArray = perpherals;
} failure:^(SEScanError error) {
NSLog(@"请检查蓝牙状态", nil);
}];
// 触发事件(不断将扫描到的外设列表传递给 RN 页面)
[self sendEventWithName:@"scanBLEList" body:self.deviceArray];
}
RN 端监听事件,
import { NativeModules, NativeEventEmitter } from 'react-native';
const NativeModule = new NativeEventEmitter(NativeModules.MCRNBlueTooth);
class ConnectBLEPrinter extends Component {
componentDidMount () {
// 获取当前设备列表
MCRNBlueTooth.scanBLEList();
this.listener = NativeModule.addListener('scanBLEList',(data) => {
console.log('输出 test' + data.list);
this.setState({ perpheralList: data.list });
});
// 检测蓝牙是否开启
NativeModules.MCRNBlueTooth.getState.then(res => {
console.log('BLE state:' + res. state);
}).catch((error) => {
});
}
}
// 移除监听
componentWillUnmount() {
NativeEventEmitter.removeListener('scanBLEList',this.listener);
}
为避免原生模块发出事件后,RN 中多次收到该事件。需要在 RN 页面声明周期结束后移除对事件的监听。
后记
最开始与原生交互使用的是 promise 的方式,但在扫描蓝牙外设的场景下通过 promise 不断将扫描(设置 10 秒的扫描超时时间)出的设备列表传递给 RN 时,RN 页面报错:
Illegal callback invocation from native module. This callback type only permits a single invocation from native code.
正如上文所说,Callbacks 和 Promises 只能传递一次数据。调用一次后就会被立即释放掉。使用 RCTEventEmitter 固然可以实现扫描外设,多次调用方法回传数据。但为了统一项目中数据的调用方式为 Promises,需要修改 Promises 调用方法中的逻辑。 可以在扫描完成后,先停止扫描,再将当前扫描结果返回给 RN 。
参考资料
–EOF–
若无特别说明,本站文章均为原创,转载请保留链接,谢谢!