RN原生模块向JS传递数据的几种

一、前言

在做React Native开发的时候避免不了的需要原生模块和JS之间进行数据传递,这篇文章将向大家分享原生模块向JS传递数据的几种方式。

二、通过Callbacks的方式

原生模块有一种特殊的参数--回调函数,在绝大多数情况下用来提供一个回调方法进行传值给JavaScript。具体该怎么样提供回调方法呢?还记得前面的文章中讲到原生模块封装给RN前端调用不?被调用的方法使用@ReactMethod注解。YES这边也差不多。我们来看一下官方的实例怎么样写的

import com.facebook.react.bridge.Callback;

public class UIManagerModule extends ReactContextBaseJavaModule {

...

  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Callback errorCallback,
      Callback successCallback) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);
      float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
      float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
      float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
      float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
      successCallback.invoke(relativeX, relativeY, width, height);
    } catch (IllegalViewOperationException e) {
      errorCallback.invoke(e.getMessage());
    }
  }

...

然后该方法在JavaScript中进行如下调用:

UIManager.measureLayout(
  100,
  100,
  (msg) => {
    console.log(msg);
  },
  (x, y, width, height) => {
    console.log(x + ':' + y + ':' + width + ':' + height);
  }
);

这样原生模块只会调用回调方法一次,但是,它可以保存 callback 并在将来调用。

请务必注意 callback 并非在对应的原生函数返回后立即被执行——注意跨语言通讯是异步的,这个执行过程会通过消息循环来进行。

具体的实例代码在文章顶部的项目地址中,实例如下:

实例使用方法在之前的项目中ToastModule添加measureLayout方法如下:

import android.widget.Toast;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.IllegalViewOperationException;

import java.util.HashMap;
import java.util.Map;

public class ToastModule  extends ReactContextBaseJavaModule {
    private static final String DURATION_SHORT_KEY = "SHORT";
    private static final String DURATION_LONG_KEY = "LONG";

    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "ToastExample";
    }

    @Override
    public Map getConstants() {
        final Map constants = new HashMap<>();
        constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
        constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
        return constants;
    }

    @ReactMethod
    public void show(String message, int duration) {
        Toast.makeText(getReactApplicationContext(), message, duration).show();
    }

    /**
     * 这边只是演示相关回调方法的使用,所以这边的使用方法是非常简单的
     * @param errorCallback       数据错误回调函数
     * @param successCallback     数据成功回调函数
     */
    @ReactMethod
    public void measureLayout(Callback errorCallback,
                              Callback successCallback){
        try {
            successCallback.invoke(100, 100, 200, 200);
        } catch (IllegalViewOperationException e) {
            errorCallback.invoke(e.getMessage());
        }
    }
}

具体前端JavaScript文件中的调用方式如下:

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
    Button
} from 'react-native';

import ToastExample from './ToastExample';

import { NativeModules } from "react-native";

export default class App extends Component {
  render() {
    return (
      
        
          RN的界面
        
        

运行截图效果:

RN原生模块向JS传递数据的几种_第1张图片

三、通过Promises的方式

看了上面的回调函数的使用,大家有没有发现上面的写法还有有一些繁琐的?OK 当然原生模块还可以支持使用Promise,这样可以简化我们编写的代码。如果大家搭配使用ES2016标准的async/await的语法使用会更加好。如果被桥接的原生方法的最后一个参数是一个Promise对象,那么该JS方法会返回一个Promise对象。下面我们使用Promise对象来进行重构之前的回调函数方法。具体官方代码如下:

在原生模块中:

import com.facebook.react.bridge.Promise;

public class UIManagerModule extends ReactContextBaseJavaModule {

...
  private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR";
  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Promise promise) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);

      WritableMap map = Arguments.createMap();

      map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0]));
      map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1]));
      map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2]));
      map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3]));

      promise.resolve(map);
    } catch (IllegalViewOperationException e) {
      promise.reject(E_LAYOUT_ERROR, e);
    }
  }

...

提示:在原生模块中Promise类型的参数要放在最后一位,这样JS调用的时候才能返回一个Promise。

在JS模块中:

那么现在你可以声明一个async的方法用来返回Promise对象,方法内部使用await关键字进行等待其返回。注意该方法还是异步的。具体代码如下:

async test() {
  try {
    var {
        relativeX,
        relativeY,
        width,
        height,
    } = await RNTest.measureLayout(100, 100);

    console.log(relativeX + ':' + relativeY + ':' + width + ':' + height);  
  } catch (e) {
    console.error(e);
  }
}

在上述代码中,通过ES7的新特性async/await来修饰了test方法,来以同步方式调用原生模块的measureLayout方法,如果原生模块处理成功, 那么JS中relativeX,relativeY,width,height会获得相应的值,如果原生模块处理失败,则会抛出异常。

如果,不希望以同步的形式调用,可以这样写:

test2(){
  RNTest.measureLayout(100,100).then(e=>{
    console.log(e.relativeX + ':' + e.relativeY + ':' + e.width + ':' + e.height);
    this.setState({
      relativeX:e.relativeX,
      relativeY:e.relativeY,
      width:e.width,
      height:e.height,
    })
  }).catch(error=>{
    console.log(error);
  });
}

上述两种方式,通过Callbacks的方式与通过Promises的方式,都可以向JS模块传递数据,但都是只能传递一次。 如果,你需要多次向JS模块传递数据(如:按键事件)上述方式还是不够好,下面就像大家分享可以多次传递数据的方式发->送事件到JavaScript中。

四、通过发送事件的方式

值得注意的是原生模块可以在没有被直接调用的情况下就可以往JavaScript发送消息事件。最简单的方式就是使用RCTDeviceEventListener接口,这可以通过ReactContext来获得对应的引用:

在原生模块中:

...
@Override
public void onHandleResult(String barcodeData) {
    WritableMap params = Arguments.createMap();
    params.putString("result", barcodeData);
    sendEvent(getReactApplicationContext(), "onScanningResult", params);
}

private void sendEvent(ReactContext reactContext,String eventName, @Nullable WritableMap params) {
    reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(eventName, params);
}

上述代码向JS模块发送了一个名为“onScanningResult”的事件,并携带了“params”作为参数。

在JS模块中:

下面是在JS代码中进行监听原生模块发出的名为“onScanningResult”的事件。

componentDidMount() {
    //注册扫描监听
    DeviceEventEmitter.addListener('onScanningResult',this.onScanningResult);
}
onScanningResult = (e)=> {
    this.setState({
        scanningResult: e.result,
    });
    // DeviceEventEmitter.removeListener('onScanningResult',this.onScanningResult);//移除扫描监听
}

在JS中通过DeviceEventEmitter注册监听了名为“onScanningResult”的事件,当原生模块发出名为“onScanningResult”的事件后,绑定在该事件上的onScanningResult = (e)会被回调。 然后通过e.result就可获得事件所携带的数据。

心得:如果在JS中有多处注册了onScanningResult事件,那么当原生模块发出事件后,这几个地方会同时收到该事件。不过大家也可以通过DeviceEventEmitter.removeListener('onScanningResult',this.onScanningResult) 来移除对名为“onScanningResult”事件的监听。

另外,JS模块也支持通过Subscribable mixin,也注册监听事件,因为ES6已经不再推荐使用mixin,所以在这里也就不向大家介绍了。

三种方式的优缺点

方式 缺点 优点
通过Callbacks的方式 只能传递一次 传递可控,JS模块调用一次,原生模块传递一次
通过Promises的方式 只能传递一次 传递可控,JS模块调用一次,原生模块传递一次
通过发送事件的方式 原生模块主动传递,JS模块被动接收 可多次传递

你可能感兴趣的:(RN原生模块向JS传递数据的几种)