React Native 作为一个混合开发解决方案,因为业务、性能上的种种原因,总是避免不了与原生进行交互。在开发过程中我们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为以下几类:
- 通过原生 Module 进行交互
- 通过原生 View 进行交互
- 通过发送事件 Event 进行交互
一、通过原生 Module 进行交互
通过原生 Module 进行交互是最高频的使用方式。封装原生 Module 可以将定义好的原生方法交给 RN 在 JS 端进行调用,JS 端可以在调用方法时通过传参的方式直接将数据传输给原生端,而原生端可以在方法执行过后将需要返回的数据通过 Promise 或者 Callback 将数据返回给 JS 端。
1.1 封装原生 Module
封装原生 Module 的步骤包括以下几步:
- 创建自定义 Module
- 创建自定义 Package 注册自定义 Module
- 注册自定义 Package
- 在RN中使用自定义 Module
1.1.1 创建自定义 Module
创建自定义 Module 其实就是将 RN 希望调用的原生功能封装成中间件的形式,这个中间件需要继承 ReactContextBaseJavaModule 类,并重写它的 getName() 方法。getName() 方法返回了 JS 可以访问的自定义 Module 名称,使得我们在 JS 端可以通过 NativeModules.自定义 Module 名称 的形式访问这个中间件。
public class MyModule extends ReactContextBaseJavaModule {
public MyModule(@Nonnull ReactApplicationContext reactContext) {
super(reactContext);
}
@Nonnull
@Override
public String getName() {
return "MyNativeModule"; // 暴露给RN的模块名,在JS端通过 NativeModules.MyNativeModule 即可访问到本模块
}
}
复制代码
1.1.2 创建自定义 Package 注册自定义 Module
自定义 Package 实现了 ReactPackage 接口,该接口提供了2个方法来分别注册自定义 Module 和自定义 ViewManager,其中自定义 ViewManager 用于封装原生 View 与 RN 进行交互,具体内容可以查看后面的“通过原生 View 进行交互”。我们需要在 createNativeModules 方法中返回一个包含我们新建的自定义 Module 实例的 List,完成注册。
public class MyReactPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyModule(reactContext)); // 将新建的 MyModule 实例加入到 List 中完成注册
return modules;
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
复制代码
1.1.3 注册自定义 Package
仅仅完成了自定义 Module 在自定义 Package 的注册还不能让 RN 使用我们的 Module,我们需要将刚刚新建的 ReactPackage 实例注册到 ReactApplication 的 ReactNativeHost 实例中。在默认的 RN 工程下,ReactApplication 通常为 MainApplication,你也可以让自己原有安卓项目中的 Application 类实现 ReactApplication 接口。
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyReactPackage() // 将新建的 MyReactPackage 实例注册到 ReactPackage 列表中
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
复制代码
1.1.4 在 RN 中使用自定义 Module
完成原生端的注册后,RN 即可使用我们封装的自定义 Module 了。在 JS 端引用的代码如下:
import { NativeModules } from "react-native";
let customModule = NativeModules.MyNativeModule; // 此处引用的自定义 Module 名必须与自定义 Module 中 getName() 方法返回的字符串一致
复制代码
接下来我们可以通过这个自定义 Module 实现 RN 与原生的几种通信方式。
1.2 JS 端获取原生端自定义 Module 预设的常量值
在自定义 Module 中,我们可以通过重写 getConstants() 方法,返回一个 Map。这个 Map 的 key 为 RN 中可以被访问的常量名称,value 为预设的常量值。
private static final String CUSTOM_CONST_KEY = "TEXT";
@Nullable
@Override
// 获取模块预定义的常量值
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(CUSTOM_CONST_KEY, "这是模块预设常量值");
return constants;
}
复制代码
在 RN 中使用 Text 组件调用这个常量显示内容:
<Text>{ customModule.TEXT }</Text>
复制代码
1.3 原生端通过 @ReactMethod 暴露方法给 JS 端,接受 JS 端调用方法时传入的参数数据
在原生端,如果想将方法暴露给 RN 调用,可以在方法前加上 @ReactMethod 注解,但要注意,这个方法的访问权限和返回类型必须要设置为 public void 才可以。
@ReactMethod
public void myFunction(String parmas) {
// To Do Something
// 字符串 params 即为 RN 传入的参数
}
复制代码
在 JS 端调用这个函数,并传递参数:
customModule.myFunction("这里是参数 params 的内容");
复制代码
注:传入的参数并不局限于例子中的 String 类型,且参数数量可以是多个。而 JS 与 Java 的类型对应如下:
JavaScript | Java |
Bool | Boolean |
Number | Integer |
Number | Double |
Number | Float |
String | String |
Function | Callback |
Object | ReadableMap |
Array | ReadableArray |
1.4 原生端通过 Callback 回调函数返回数据给 JS 端
通过上面的类型对应,我们了解到, JS 中的 function 作为参数传输到 Java 中就是个 Callback。所以我们可以使用回调函数,在完成原生方法的执行过后,将需要的结果或状态通过 Callback.invoke() 方法回调给 JS。
@ReactMethod
public void myFunction(String params, Callback success, Callback failture) {
try {
if (params != null && !params.equals("")){
// 回调成功,返回结果信息
success.invoke("这是从原生", "返回的字符串");
}
}catch (IllegalViewOperationException e) {
// 回调失败,返回错误信息
failture.invoke(e.getMessage());
}
}
复制代码
在 JS 中需要定义回调函数的执行内容,这里定义了一个匿名函数作为回调函数。你可以根据自己的业务需求替换为相应的回调函数。
customModule.myFunction(
"这是带Callback回调的函数方法",
(parma1, parma2) => {
var result = parma1 + parma2;
console.log(result); // 显示: 这是从原生返回的字符串
},
errMsg => {
console.log(errMsg);
}
);
复制代码
1.5 原生端通过 Promise 函数返回数据给 JS 端
除了使用 Callback 进行回调,我们还可以在 ReactMethod 中将 Promise 作为最后一个参数,使用 Promise 实例,完成数据的回传。Promise 具有 resolve() 和 reject() 两个方法,可以用于处理正常和异常的回传。在使用时,我们通常在自定义 Module 中定义一个 Promise 类型的私有变量,在调用 ReactMethod 时,对这个私有变量进行赋值,然后在需要回传数据的地方使用这个私有变量进行回传,这样可以更灵活的控制回传时机。但要注意的是,Promise 实例只能被 resolve() 或 reject() 一次,若多次回调将会报错。
private Promise mPromise;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mPromise.resolve((String) msg.obj);
}
};
@ReactMethod
public void myFunction(String params, Promise promise) {
mPromise = promise;
try {
if (params != null && !params.equals("")){
// 回调成功,返回结果信息
Message message = handler.obtainMessage();
message.obj = params;
handler.sendMessage(message);
}
}catch (IllegalViewOperationException e) {
// 回调失败,返回错误信息
mPromise.reject('error', e.getMessage());
}
}
复制代码
在 JS 端的调用情况
customModule.myFunction("这是使用 Promise 回调的函数方法")
.then(result => {
console.log(result); // 显示: 这是从原生返回的字符串
})
复制代码
二、通过原生 View 进行交互
通过自定义 Module 进行交互已经可以解决我们的大部分开发需求,然而有的时候,基于性能和开发工作量的角度考虑,我们可以将原生的组件或布局封装好,并为这个原生 View 建立一个继承自 SimpleViewManager 或 ViewGroupManager 的 ViewManager 类。通过这个 ViewManager 可以注册一系列原生端和 JS 端的参数及事件映射,达到交互的目的。
2.1 封装原生 View
封装原生 View 包括以下几步:
- 创建原生 View 类
- 创建 ViewManager
- 将 ViewManager 在自定义 Package 中注册
- 在 Js 端进行调用
2.1.1 创建原生 View 类
这里以封装一个简单的原生 Button 的子类为例:
public class MyButton extends Button {
public MyButton(Context context) {
super(context);
}
}
复制代码
2.1.2 创建相应的 ViewManager 类
简单的 View 可以创建 ViewManager 类继承 SimpleViewManager ,而通过布局生成的复杂 View 可以继承自 ViewGroupManager 类,这里我们继承 SimpleViewManager:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton"; // 此名称用于在 JS 中引用
}
// 创建 View 实例
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
return new MyButton(reactContext);
}
}
复制代码
2.1.3 将 ViewManager 在自定义 Package 中注册
之前我们创建了自定义 Package 类 MyReactPackage,我们只是使用了 createNativeModules() 方法完成了自定义 Module 的注册,接下来我们需要在 createViewManagers() 方法中注册刚刚创建的 MyButtonViewManager 实例:
public class MyReactPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyModule(reactContext));
return modules;
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
List<ViewManager> views = new ArrayList<>();
views.add(new MyButtonViewManager()); // 创建 MyButtonViewManager 实例并注册到 ViewManager List 中
return views;
}
}
复制代码
注意,如果你没有完成第一章中 Package 在 Application 的注册步骤,这里也需要将 MyReactPackage 注册到 Application 中。
2.1.4 在 JS 端完成调用
完成了上面的原生代码,我们就可以在 JS 端完成调用了:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton/>
);
}
...
复制代码
2.2 原生端通过 @ReactProps 将方法暴露给 JS 端,接收 JS 端为组件设定的属性
在刚刚建立的 ViewManager 类中,我们可以通过 @ReactProps 注解方法,为组件添加属性,这里我们为 MyButton 添加 text 属性:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
return MyButton(reactContext);
}
// 暴露给 JS 的参数,用于设定名称为“text”的属性,设定 Button 的文字
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
}
复制代码
此时可以在 JS 端为组件添加 text 属性和它的值,完成设定 MyButton 的文字:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton text='这是个按钮'/>
);
}
复制代码
2.3 原生端通过注册 View 事件与 JS 端的映射,使 JS 端可以接收原生端发送的事件和数据
有些时候我们不仅仅需要将数据以属性值的形式,从 JS 端传输到原生端,还需要原生端对 JS 端发送数据完成交互,这时我们需要使用事件 Event,通过发送事件完成交互。这里我们可以将 MyButton 的点击事件通知给 JS 端:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
MyButton button = new MyButton(reactContext);
button.setOnClickListener(v -> {
WritableMap event = Arguments.createMap(); // 这里传了个空的 event 对象,使用时可以在 event 中加入要传输的数据
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
viewId,
"onNativeClick", // 与下面注册的要发送的事件名称必须相同
event);
});
return button;
}
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
// onNativeClick 是原生要发送的 event 名称,onReactClick 是 JS 端组件中注册的属性方法名称,中间的 registrationName 不可更改
}
}
复制代码
然后在 JS 端就可以使用 onReactClick 这个属性来响应点击事件:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton text='这是个按钮' onReactClick={data=>{
// 这里接收 event 传过来的数据
console.log(data);
}}/>
);
}
复制代码
2.4 原生端通过注册 View 命令表和响应方法,完成接收来自 JS 端的指令
JS 端不仅仅只能从设定属性值的方法来将数据传输给原生端,也可以使用 UIManager.dispatchViewManagerCommand 方法来发送命令并携带数据给原生端,这里我们添加了一条命令 changeText 让 MyButton 点击后更换按钮上的文字:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
changeButtonText = () => {
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.nativeUI),
UIManager.TemplateMenuView.Commands.changeText, //Commands.changeText需要与native层定义的命令名称一致
['这是新的按钮'] //命令携带的数据
);
}
render() {
return (
<MyButton
ref={view => this.nativeUI = view}
text='这是个按钮'
onReactClick={data=>{
this.changeButtonText; // 点击时回传给原生端命令
}}/>
);
}
复制代码
在原生端,我们需要在 ViewManager 中重写 getCommandsMap() 方法建立命令的映射表,然后重写 receiveCommand() 方法完成接收命令后的操作
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
MyButton button = new MyButton(reactContext);
button.setOnClickListener(v -> {
WritableMap event = Arguments.createMap();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
viewId,
"onNativeClick",
event);
});
return button;
}
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
}
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
“changeText”, 0 // changeText 是命令名称,0 是命令 id
);
}
@Override
public void receiveCommand(MyButton view, int commandId, @Nullable ReadableArray args) {
switch (commandId){
case 0: // 当命令 id 为 0 时
String newText = args.getString(0); // 我们在 JS 只传了一个字符串过来,在这里接收
view.setText(newText); // 为按钮设定新的文字
break;
default:
break;
}
}
}
复制代码
三、通过发送事件 Event 进行交互
利用原生View进行交互的时候,我们已经利用了发送事件的机制,完成原生端与 JS 端的通信,但前提是需要将原生 View 的属性方法与原生事件先注册映射,才能获取这个事件。其实事件 Event 是可以通过 RCTDeviceEventEmitter 灵活完成发送的,原生端将事件名称和事件数据发送后,JS 端需要根据事件名称预先注册一个监听器,来响应这个接收的事件,这也是最为灵活的一种交互方式。
3.1 在原生端通过 RCTDeviceEventEmitter 发送事件
//定义向 RN 发送事件的函数
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName,params);
}
// 发送成功消息
public notifySuccessMessage(ReactContext reactContext, String msg) {
WritableMap event = Arguments.createMap();
event.putString("message", msg);
sendEvent(reactApplicationContext, "SUCCESS", event);
}
// 发送失败消息
public notifyErrorMessage(ReactContext reactContext) {
WritableMap event = Arguments.createMap();
sendEvent(reactApplicationContext, "ERROR", event);
}
复制代码
3.2 在 JS 端注册监听器,并在合适的时机移除监听器
componentDidMount() {
// 收到监听
this.listener = DeviceEventEmitter.addListener('SUCCESS', (message) => {
// 收到监听后想做的事情,’SUCCESS‘ 必须与原生层传递的 eventName 一致
console.warn(message);
dosomething...
});
this.errorListener = DeviceEventEmitter.addListener('ERROR', (message) => {
// 收到监听后想做的事情,’ERROR‘ 必须与原生层传递的 eventName 一致
console.warn(message);
dosomething...
});
}
componentWillUnmount() {
// 移除监听
if (this.listener) { this.listener.remove() }
if (this.errorListener) { this.listener.remove() }
}
复制代码
以上可以看出发送事件可以在任何时机,调用 sendEvent() 方法即可。但并不是任何时机都可以顺利获得 ReactContext 上下文对象,所以最好将发送事件的方法封装成一个工具类,在 App 生命周期较早的时机进行初始化,传入 ReactContext 上下文对象,然后即可在想发送事件的时机,只传递事件名和数据即可。
Update:
上面的写法中在 Js 端使用的是 DeviceEventEmitter.addListener(eventName, function) 方法注册的监听器,这是因为安卓端使用了 DeviceEventManagerModule.RCTDeviceEventEmitter.class 类完成的事件发送。但 iOS 端发送事件时,使用上面的方法注册监听器是无法响应的,这是因为在发送事件时使用的是 RCTEventEmitter 类,这个类中也是调用了 RCTDeviceEventEmitter 类完成的事件发送,但在发送前检查了注册的监听器数量:
if (_listenerCount > 0) {
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
method:@"emit"
args:body ? @[eventName, body] : @[eventName]
completion:NULL];
}
复制代码
为了兼容两端,所以 JS 端应该使用 NativeEventEmitter.addListener(eventName, function) 方法来注册监听器,完整的 JS 端代码:
const eventEmitter = NativeModules.EventEmitter;
const nativeEmitter = new NativeEventEmitter(eventEmitter);
const subscription = nativeEmitter.addListener(
eventEmitter.SUCCESS,
message => {
console.warn(message);
dosomething...
}
);
复制代码
可以看出 JS 端的代码是通过使用自定义 Module 完成的监听器注册,所以安卓端也应该增加一个自定义 Module,代码如下:
public class ReactEventEmitterModule extends >ReactContextBaseJavaModule {
public ReactEventEmitterModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
constants.put("SUCCESS", "ThisIsSuccessConstant");
return constants;
}
@Override
public String getName() {
return "EventEmitter";
}
}
复制代码