一、前言
在做React Native开发的时候避免不了的需要原生??楹蚃S之间进行数据传递,这篇文章将向大家分享原生模块向JS传递数据的几种方式。
二、通过Callbacks的方式
原生??橛幸恢痔厥獾牟问?-回调函数,在绝大多数情况下用来提供一个回调方法进行传值给JavaScript。具体该怎么样提供回调方法呢?还记得前面的文章中讲到原生??榉庾案鳵N前端调用不?被调用的方法使用@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);
}
}
...
提示:在原生??橹蠵romise类型的参数要放在最后一位,这样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中。
四、通过发送事件的方式
值得注意的是原生??榭梢栽诿挥斜恢苯拥饔玫那榭鱿戮涂梢酝鵍avaScript发送消息事件。最简单的方式就是使用RCTDeviceEventListener
接口,这可以通过ReactContext
来获得对应的引用:
在原生??橹校?/strong>
...
@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??榉⑺土艘桓雒皁nScanningResult”的事件,并携带了“params”作为参数。
在JS??橹校?/strong>
下面是在JS代码中进行监听原生??榉⒊龅拿皁nScanningResult”的事件。
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??橐仓С滞ü齋ubscribable mixin,也注册监听事件,因为ES6已经不再推荐使用mixin,所以在这里也就不向大家介绍了。
三种方式的优缺点
方式 | 缺点 | 优点 |
---|---|---|
通过Callbacks的方式 | 只能传递一次 | 传递可控,JS??榈饔靡淮危?榇菀淮?/td> |
通过Promises的方式 | 只能传递一次 | 传递可控,JS??榈饔靡淮危?榇菀淮?/td> |
通过发送事件的方式 | 原生??橹鞫?,JS??楸欢邮?/td> | 可多次传递 |