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<String, Object> getConstants() {
        final Map<String, Object> 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 (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          RN的界面
        </Text>
        <Button
            title='Toast'
            onPress={()=>{
                ToastExample.show("Awesome", ToastExample.SHORT);
            }}/>
        <Button
            title='measureLayout'
            onPress={()=>{
                ToastExample.measureLayout((msg)=>{
                    ToastExample.show(x + '坐标,' + y + '坐标,' + w + '宽,' + h+'高', ToastExample.SHORT);
                },(x,y,w,h)=>{
                    ToastExample.show(x + '坐标,' + y + '坐标,' + w + '宽,' + h+'高', ToastExample.SHORT);
                })
            }}/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
 });

运行截图效果:

三、通过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模块被动接收 可多次传递
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容