React Native 做为一个混合开发解决方案,由于业务、性能上的种种缘由,老是避免不了与原生进行交互。在开发过程当中咱们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为如下几类:javascript
经过原生 Module 进行交互是最高频的使用方式。封装原生 Module 能够将定义好的原生方法交给 RN 在 JS 端进行调用,JS 端能够在调用方法时经过传参的方式直接将数据传输给原生端,而原生端能够在方法执行事后将须要返回的数据经过 Promise 或者 Callback 将数据返回给 JS 端。java
封装原生 Module 的步骤包括如下几步:react
- 建立自定义 Module
- 建立自定义 Package 注册自定义 Module
- 注册自定义 Package
- 在RN中使用自定义 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 便可访问到本模块
}
}
复制代码
自定义 Package 实现了 ReactPackage 接口,该接口提供了2个方法来分别注册自定义 Module 和自定义 ViewManager,其中自定义 ViewManager 用于封装原生 View 与 RN 进行交互,具体内容能够查看后面的“经过原生 View 进行交互”。咱们须要在 createNativeModules 方法中返回一个包含咱们新建的自定义 Module 实例的 List,完成注册。react-native
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();
}
}
复制代码
仅仅完成了自定义 Module 在自定义 Package 的注册还不能让 RN 使用咱们的 Module,咱们须要将刚刚新建的 ReactPackage 实例注册到 ReactApplication 的 ReactNativeHost 实例中。在默认的 RN 工程下,ReactApplication 一般为 MainApplication,你也可让本身原有安卓项目中的 Application 类实现 ReactApplication 接口。promise
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);
}
}
复制代码
完成原生端的注册后,RN 便可使用咱们封装的自定义 Module 了。在 JS 端引用的代码以下:微信
import { NativeModules } from "react-native";
let customModule = NativeModules.MyNativeModule; // 此处引用的自定义 Module 名必须与自定义 Module 中 getName() 方法返回的字符串一致
复制代码
接下来咱们能够经过这个自定义 Module 实现 RN 与原生的几种通讯方式。ide
在自定义 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>
复制代码
在原生端,若是想将方法暴露给 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 |
经过上面的类型对应,咱们了解到, 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);
}
);
复制代码
除了使用 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); // 显示: 这是从原生返回的字符串
})
复制代码
经过自定义 Module 进行交互已经能够解决咱们的大部分开发需求,然而有的时候,基于性能和开发工做量的角度考虑,咱们能够将原生的组件或布局封装好,并为这个原生 View 创建一个继承自 SimpleViewManager 或 ViewGroupManager 的 ViewManager 类。经过这个 ViewManager 能够注册一系列原生端和 JS 端的参数及事件映射,达到交互的目的。
封装原生 View 包括如下几步:
- 建立原生 View 类
- 建立 ViewManager
- 将 ViewManager 在自定义 Package 中注册
- 在 Js 端进行调用
这里以封装一个简单的原生 Button 的子类为例:
public class MyButton extends Button {
public MyButton(Context context) {
super(context);
}
}
复制代码
简单的 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);
}
}
复制代码
以前咱们建立了自定义 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 中。
完成了上面的原生代码,咱们就能够在 JS 端完成调用了:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton/>
);
}
...
复制代码
在刚刚创建的 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='这是个按钮'/> ); } 复制代码
有些时候咱们不只仅须要将数据以属性值的形式,从 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); }}/> ); } 复制代码
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;
}
}
}
复制代码
利用原生View进行交互的时候,咱们已经利用了发送事件的机制,完成原生端与 JS 端的通讯,但前提是须要将原生 View 的属性方法与原生事件先注册映射,才能获取这个事件。其实事件 Event 是能够经过 RCTDeviceEventEmitter 灵活完成发送的,原生端将事件名称和事件数据发送后,JS 端须要根据事件名称预先注册一个监听器,来响应这个接收的事件,这也是最为灵活的一种交互方式。
//定义向 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);
}
复制代码
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"; } } 复制代码
原文连接: tech.meicai.cn/detail/96 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。