React Native 原生模块与 JS 模块交互

Author Avatar
XibHe 3月 17, 2019
  • 在其它设备中阅读本文章

最近使用 RN 时,需要在 RN 端实现调用系统相册上传选中照片的功能。需要原生模块与 RN 模块进行交互,主要步骤:

  1. 调用原生系统相;
  2. 将选取的照片存到本地沙盒;
  3. 再将沙盒路径传给 RN。

注意: 这里之所以需要将获取的照片存到沙盒下,再将沙盒路径传递给 RN 是因为 RN 端不支持 iOS 中 NSData 二进制流的数据传输格式。而是通过 multipart/form-data 这种表单方式上传文件。

FormData 对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。

在原生与 RN 交互的过程中尝试了几种传递数据的不同方式,这里记录一下,其本质上都是通过实现 RCTBridgeModule 的协议来进行数据交互的。

为了实现 RCTBridgeModule 协议,你的类需要包含 RCT_EXPORT_MODULE() 宏。这个宏也可以添加一个参数用来指定在 Javascript 中访问这个模块的名字。你必须明确的声明要给 Javascript 导出的方法,否则 RN 不会导出任何方法。

按传递数据的不同方式,分为以下三种:

  1. Callbacks 回调函数;
  2. 原生模块使用 promise 简化代码;
  3. 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 。

参考资料

docs/native-modules-ios

React Native原生模块向JS传递数据的几种方式

FormData 对象的使用

在React Native Module数据多次回调

–EOF–

若无特别说明,本站文章均为原创,转载请保留链接,谢谢!