内容简介:React Native 作为一个混合开发解决方案,因为业务、性能上的种种原因,总是避免不了与原生进行交互。在开发过程中我们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为以下几类:通过原生 Module 进行交互是最高频的使用方式。封装原生 Module 可以将定义好的原生方法交给 RN 在 JS 端进行调用,JS 端可以在调用方法时通过传参的方式直接将数据传输给原生端,而原生端可以在方法执行过后将需要返回的数据通过 Promise 或者 Callback 将数据返回给 JS 端。封装原生 Mo
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"; } } 复制代码
原文链接: tech.meicai.cn/detail/96 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。
以上所述就是小编给大家介绍的《「React Native」与「Android」的交互方式总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- js和原生应用常用的数据交互方式
- GraphQL.js 与服务端交互的新方式
- Git 2.22 发布,改进处理合并 base 的交互方式
- 最全面总结 Android WebView 与 JS 的交互方式(含实例 Demo)
- iOS 12 人机交互指南:交互(User Interaction)
- 生活NLP云服务“玩秘”站稳人机交互2.0语音交互场景
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
勇敢新世界‧互聯網罪與罰
許煜、劉細良 / CUP / 2005 / $48
我天天上網數小時,為的是要在節目裡面介紹世界的最新動態,尤其是網絡這個世界本身日新月異的變化。所以我不可能不注意到BT、共享軟件、 Wikipedia、網絡監管等各種影響政治、社會、經濟及文化的重要網絡現象。但是我發現市面上一直沒有一本內容充實全面,資料切時的中文參考書,直到這本《互聯網罪與罰》。而且,最大的驚喜是它易讀好看,簡直就像故事書。 梁文道 鳳凰衛視 《網羅天下......一起来看看 《勇敢新世界‧互聯網罪與罰》 这本书的介绍吧!