前段时间,由于业务的需求,开始接触到了 RN 原生模块和组件的开发,最近刚好有一位同事也是有这方面的需求要开发就过来问我,我一时间竟然有点说不清楚,回想一下,挺多一些点已经有些模糊了,趁着现在刚过完年回来,需求还不多的时候,按照官方的文档又重新理了一遍。

原生模块

我们做 RN 需求的过程,有时候需要拿一些硬件方面的数据,比如蓝牙、NFC,又或者人脸、指纹等数据,这些可能还会有些第三方库做这些东西,但是像我们自己业务的一些用户体系的一些数据,我们需要一套规范的 API ,这个时候就是原生模块上场的时机,我们可以定义一套获取用户体系的 API 接口,并且加上一些控制的逻辑来方便和规范 RN 的调用。

下面就以官方的例子来说明如何接入 原生模块 吧。

ReactContextBaseJavaModule

首先我们需要一个 Module ,我们后面在 RN 调用的这个模块的方法就来自这个类。我们先创建一个类,继承 ReactContextBaseJavaModule

public class CalendarModule extends ReactContextBaseJavaModule {CalendarModule(ReactApplicationContext context) {super(context);}
}

getName()

ReactContextBaseJavaModule 这个类是一个抽象类,但是只需要实现一个方法。

String getName();

getName() 这个方法会返回一个 String 这个就是我们模块的名称了。

@Override
public String getName() {return "CalendarModule";
}

实现之后,我们后面就可以像下面这样在 RN 中调用这个模块了。

const { CalendarModule } = ReactNative.NativeModules;

或者,下面这样也可以拿到这个模块对象。

const CalendarModule = NativeModules.CalendarModule

其实这两种方法本质是一样的,只是第一种方式是用了解构赋值的语法。

@ReactMethod

对于想要暴露出去的方法,只需要在方法的上面添加 @ReactMethod 这个注解就可以了,但是要记得方法的访问权限是 public 的。

@ReactMethod
public void createCalendarEvent(String name, String location) {
}

还有一点要注意,RN 调用原生模块的时候是异步的,也就是说,调用之后就会走 RN 的后续逻辑,并不会等待原生代码执行完成。

@ReactMethod(isBlockingSynchronousMethod = true)

刚才说到一般的 RN 调用是异步的,如果有同步调用的需求,可以在 @ReactMethod 这个注解里面添加 isBlockingSynchronousMethod = true 这个参数,这样的话,调用过程就是同步的了。

但是官方不是很建议我们使用这个,主要还是考虑到有些开发者会误用,导致性能问题,或者造成线程相关的bug。

另外,如果使用同步调用这种方式,就无法使用 Chrome 进行 debug 了。

注册模块

Module 写完了,但是现在 RN 还不能调用,因为还需要一个注册的过程。

ReactPackage

注册需要使用到 ReactPackage ,因为有时候我们可能会有好几个 Module ,需要我们把 Module 打包进 ReactPackage 里面,然后再进行注册。在 RN 初始化的阶段,会扫描所有注册进来的 ReactPackage ,并进行初始化。

同样的,我们需要一个类,实现 ReactPackage 。然后在 createNativeModules() 里面返回含有 ReactContextBaseJavaModule 的集合。

public class MyAppPackage implements ReactPackage {@Overridepublic List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {return Collections.emptyList();}@Overridepublic List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {List<NativeModule> modules = new ArrayList<>();modules.add(new CalendarModule(reactContext));return modules;}}

如果你细心的话,还会发现有个 createViewManagers() 的方法,这个是用来返回 原生组件 的,后面再讲。

ReactNativeHost

现在我们 ReactNativePage 包也有了,就只剩最后一个问题了。如何注册?

其实也很简单,我们首先要找到 ReactNativeHost ,你可以简单理解为 RN 的容器,就像 Android 的 Activity 和 iOS 的 View Controller。如果你是用 RN 默认的构建脚本创建的项目,ReactNativeHost 一般是 MainApplication.java 这个文件。

找到之后就简单了。只需要把我们的 package 塞进 getPackages() 返回的 list 里面就可以了。

@Overrideprotected List<ReactPackage> getPackages() {@SuppressWarnings("UnnecessaryLocalVariable")List<ReactPackage> packages = new PackageList(this).getPackages();// below MyAppPackage is added to the list of packages returnedpackages.add(new MyAppPackage());return packages;}

到这里,就已经可以写个 demo ,试一下能不能调到这个模块的方法了。记得要重新启动 RN 。

import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules;
...
const onPress = () => {CalendarModule.createCalendarEvent('testName', 'testLocation');
};

更好用的原生模块

按照上面的例子确实可以跑起来,但是你会发现有点问题,就是 CalendarModule 是没有类型检查的,而且如果每次到需要通过 NativeModules 来获取原生模块,这种方式,按照官方来说就是有点 clunky

所以为了使用起来更加舒服,我们一般都会用 js 或者 ts 再包上一层,这样其他地方使用的时候就会更加方便,而且还可以有类型检查。

/**
* This exposes the native CalendarModule module as a JS module. This has a
* function 'createCalendarEvent' which takes the following parameters:
*
* 1. String name: A string representing the name of the event
* 2. String location: A string representing the location of the event
*/
import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules
interface CalendarInterface {createCalendarEvent(name: string, location: string): void;
}
export default CalendarModule as CalendarInterface;

然后在其他的 RN 中使用的话就更加简单和舒服了。

import CalendarModule from './CalendarModule';
CalendarModule.createCalendarEvent('foo', 'bar');

类型映射

我们都知道在跨平台的时候,很多数据类型是不通用的,很多时候靠谱的就只有 字符串 了。RN 为了更方便我们 RN 和 原生 的互调,做了数据类型的映射。

JAVA JAVASCRIPT
Boolean ?boolean
boolean boolean
Double ?number
double number
String string
Callback Function
ReadableMap Object
ReadableArray Array

还有几种类型也是支持的,但是未来新的 原生模块集成方式TurboModules 并不支持,所以,如果使用的是 TurboModules ,要注意避免使用。

  • Integer -> ?number
  • int -> number
  • Float -> ?number
  • float -> number

导出常量

原生的模块也是支持导出常量的,使用也非常简单。

在 Module 中,重写 getConstants() 这个方法,然后再把这个在返回的 map 里面插入需要的导出的常量就可以了。

@Override
public Map<String, Object> getConstants() {final Map<String, Object> constants = new HashMap<>();constants.put("DEFAULT_EVENT_NAME", "New Event");return constants;
}

使用的时候可以直接调用 getConstants() 来获取。

const { DEFAULT_EVENT_NAME } = CalendarModule.getConstants();
console.log(DEFAULT_EVENT_NAME);

回调

由于模块函数的调用一般都是异步的,也就无法返回参数,如果我们要返回数据,这个时候就要使用回调了。

RN 也是支持回调的,对应的类是 com.facebook.react.bridge.Callback 。RN 支持最多两个回调函数,对应 成功回调失败回调。并且最后一个参数如果是函数,会被当做成功回调,倒数第二个参数如果是函数,就会被当成失败回调。

@ReactMethod
public void createCalendarEvent(String name, String location, Callback myFailureCallback, Callback mySuccessCallback) {}

RN 中这样调用:

const onPress = () => {CalendarModule.createCalendarEventCallback('testName','testLocation',(error) => {console.error(`Error found! ${error}`);},(eventId) => {console.log(`event id ${eventId} returned`);});
};

但其实在项目开发中,我们使用两个回调的场景也不是很多,因为很多时候跟业务之间可能会约定其他的一些状态,一般会自己定数据类型,比如会包含一个 code 或者 另一个表示状态的枚举值。然后业务再根据这个状态做对应的业务。

官方也说到了这种类似的处理方式:

@ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {Integer eventId = ...callBack.invoke(null, eventId);
}

然后再回调里面判断状态再处理。

const onPress = () => {CalendarModule.createCalendarEventCallback('testName','testLocation',(error, eventId) => {if (error) {console.error(`Error found! ${error}`);}console.log(`event id ${eventId} returned`);});
};

还有一点要注意,就是回调传过去的数据类型是要支持序列化的。另外,不能同时调用 成功回调 和 失败回调,而且成功回调 和 失败回调 只能调用一次,我感觉底层就是用了类似 Promises 一样的机制吧,一旦状态确定,就无法改变了。但是可以把 回调 储存起来,后面有结果之后再调用。

Promises

还记得我们之前讲过的 Promises 吗?一种可以让我们以一种看似同步的代码方式来写异步的逻辑。现在 RN 也支持了。

import com.facebook.react.bridge.Promise;@ReactMethod
public void createCalendarEvent(String name, String location, Promise promise) {try {Integer eventId = ...promise.resolve(eventId);} catch(Exception e) {promise.reject("Create Event Error", e);}
}

RN 中接收到的就是一个 Promises 的类型了。

const onSubmit = async () => {try {const eventId = await CalendarModule.createCalendarEvent('Party','My House');console.log(`Created a new event with id ${eventId}`);} catch (e) {console.error(e);}
};

向 RN 发事件

有些情况下,我们需要向 RN 发一些事件,比如我们的日历例子中,现在需要提醒用户现在是某个日历事项,要做什么事情。这种情况就需要原生模块向 RN 发事件,而不是等着 RN 来轮询。

RCTDeviceEventEmitter

想要向 RN 发事件,需要一个发射器,原生代码 中使用的是 RCTDeviceEventEmitter 这个。

RCTDeviceEventEmitter这个又是从哪里来呢?

还记得我们一开始写 Module 吗?构造方法里面有 ReactApplicationContext ,用这个就可以了,我们可以在构造方法里面把这个保存起来,后面需要的时候再拿出来用就好了。

需要发事件的时候,就只需要向下面这样用就好了。

WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);

RN 端要接收事件的话,需要注册一个监听。

componentDidMount() {...const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample);this.eventListener = eventEmitter.addListener('EventReminder', (event) => {console.log(event.eventProperty) // "someValue"});...}componentWillUnmount() {this.eventListener.remove(); //Removes the listener}

其他

RN 还支持监听 ActivitystartActivityForResult 事件,但是我觉得这种场景应用不是很多,一般都是由原生去做对应的操作,然后再发事件给 RN,这里就不讲了。

原生组件

大概讲完了 原生模块,接下来讲一下 原生组件。我们知道 RN 提供了很多的组件给开发者使用,而且还可以导入第三方的一些组件,大部分的场景下已经能够满足我们开发的需求了。但是在某些特殊的场景或者特殊的需求,或者是一些第三方的组件不够灵活,不满足需求的,可能也需要我们开发一些原生的组件提供给 RN 使用。

ViewManager

和 原生模块 类似的,要创建一个 原生组件 需要使用 ViewManager 来承载。ViewManager 是一个抽象类,我们一般继承其子类 SimpleViewManager 来做开发。

官方使用了 ImageView 来做例子,我们也用这个例子来看一下吧。

public class ReactImageManager extends SimpleViewManager<ReactImageView> {public static final String REACT_CLASS = "RCTImageView";ReactApplicationContext mCallerContext;public ReactImageManager(ReactApplicationContext reactContext) {mCallerContext = reactContext;}@Overridepublic String getName() {return REACT_CLASS;}@Overridepublic ReactImageView createViewInstance(ThemedReactContext context) {return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);}
}

可以看到,也是有一个 getName() 的方法,用来返回组件的名称。

createViewInstance

除了 getName() ,还有一个 createViewInstance() 的方法,这个就是实际创建组件的地方,这个方法最终会返回一个 View,最终展示的就是这个 View。

public abstract class ViewManager<T extends View, C extends ReactShadowNode> extends BaseJavaModule{...@Nonnullprotected abstract T createViewInstance(@Nonnull ThemedReactContext var1);...
}

@ReactProp

仅仅创建出 View 还不行呀,我们还需要可以动态配置的属性。

RN 中使用了 @ReactProp 这个注解来解析对应的属性,类似下面这样:

  @ReactProp(name = "src")public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {view.setSource(sources);}@ReactProp(name = "borderRadius", defaultFloat = 0f)public void setBorderRadius(ReactImageView view, float borderRadius) {view.setBorderRadius(borderRadius);}@ReactProp(name = ViewProps.RESIZE_MODE)public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {view.setScaleType(ImageResizeMode.toScaleType(resizeMode));}

当 RN 中更新 属性 的时候,就会触发对应的方法。

@ReactProp 这个注解至少需要一个 name 属性,类型是 String ,用来说明,这个属性在 RN 中对应的那个属性。

除此之外,还可以添加一个默认值,支持 defaultBoolean, defaultInt, defaultFloat 这几种类型。如果 RN 中把某个属性删掉之后,也会触发对应的方法,并传入默认值。如果是对象类型,则会传 null 。

@ReactProp 这个注解对被注解的方法也有要求,必须的 public void 的类型,并且第一个参数需要是 createViewInstance() 这个方法返回的类型,也就是当前 View 对象,第二个参数也只支持几种类型,boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap

注册 ViewManager

之前我们说过,注册 Module 需要用到 ReactPackage ,也提了一嘴,注册原生组件也会用到,这里就来了。

public class MyAppPackage implements ReactPackage {@Overridepublic List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {return Arrays.<ViewManager>asList(new ReactImageManager(reactContext));}@Overridepublic List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {return Collections.emptyList();}}

其实如果需求里面同时需要 原生模块 和 原生组件 ,可以写在同一个 Package 里面。

注册完之后,记得也要把 Package 注册一下,然后重启 RN ,应该就可以用了。RN 可以这样使用:

const RCTImageView = requireNativeComponent('RCTImageView');
...
<RCTImageViewsrc="https://xxxx"borderRadius=10 />

跟 原生模块 一样,导进来直接使用是没有类型检查的,我们同样可以再包一层,加上类型限制,后面使用的时候就可以有类型检查了。

原生向 RN 发事件

其实对于组件,不仅仅是根据参数来系那是不同的内容,有时候还需要一些交互效果,比如用户点击了这个组件,或者一个视频组件,需要通知使用方当前的播放进度。

第一个问题是,RN 要怎么接收?

RN 中是这么处理的,在 Manager 中重写 getExportedCustomBubblingEventTypeConstants() 返回 原生组件 发的事件名称,还有 RN 接收该事件的对属性名称。

public class ReactImageManager extends SimpleViewManager<MyCustomView> {...public Map getExportedCustomBubblingEventTypeConstants() {return MapBuilder.builder().put("topChange",MapBuilder.of("phasedRegistrationNames",MapBuilder.of("bubbled", "onChange"))).build();}
}

根据例子中来讲,就是原生的组件会发一个 topChange 的事件,RN 中要接收的属性名称就是 onChange 这个。这个结构是套了几层的 map ,写的时候记得别写错了。至于里面的 phasedRegistrationNamesbubbled,就是固定的写法,照着例子中这样写就好了。

还有关键的一步,就是原生的组件要怎么把事件发给 RN?

还记得 RCTDeviceEventEmitter 吗?这个是 Module 用来发事件 的,也跟 Module 类似,Manager 用的是 RCTEventEmitter 这个。原生组件 和 RN 通过 getId() 关联起来,也就是这个事件是发给某个特定的 View 。

WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "topChange", event);

但是看到 receiveEvent 的时候,一开始也感到有点奇怪,Module 中使用的是 emit。发事件嘛,用 emit 正常一点。但是官方这么写,我只能理解为 RN 接收了一条事件。

RN 调用可以这样:

class MyCustomView extends React.Component {constructor(props) {super(props);this._onChange = this._onChange.bind(this);}_onChange(event) {if (!this.props.onChangeMessage) {return;}this.props.onChangeMessage(event.nativeEvent.message);}render() {return <RCTMyCustomView {...this.props} onChange={this._onChange} />;}
}
MyCustomView.propTypes = {/*** Callback that is called continuously when the user is dragging the map.*/onChangeMessage: PropTypes.func,...
};const RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);

RN 向 原生 发事件

除了 原生 向 RN 发事件,还有一种情况是 RN 向 原生 发事件,这样两端就能互相通信了。比如你做了一个视频组件,RN 端想要跳转到某个时间点或者暂停、继续等情况。

返回支持的事件

在 Manager 中重写 getCommandsMap() 方法,在里面返回支持的事件。这是一个 Map ,可以配置多个事件。

@Override
public Map<String, Integer> getCommandsMap() {return MapBuilder.of("play", COMMAND_PLAY);}

处理事件

在 Manager 中重写 receiveCommand() 方法,在这里面处理事件。

@Overridepublic void receiveCommand(@NonNull FrameLayout root,String commandId,@Nullable ReadableArray args) {super.receiveCommand(root, commandId, args);switch (commandIdInt) {case COMMAND_PLAY:// 处理事件doSomething(root);break;default: {}}}

RN 端调用

原生端这样就搞完了,RN 端要怎么调用呢?

第一步,要获取对应的 ViewId,所以就需要使用 ref 来获取对应的组件对象的引用,然后再通过 findNodeHandle(ref) 来获取到 ViewId。

第二步,通过 UIManager.dispatchViewManagerCommand() 来发送对应的事件。

import React, { useEffect, useRef } from 'react';
import { UIManager, findNodeHandle } from 'react-native';import { MyViewManager } from './my-view-manager';// 调用这个方法就可以向 原生组件 发事件
const doSomething = (viewId) =>UIManager.dispatchViewManagerCommand(viewId,UIManager.MyViewManager.Commands.play.toString(),// 这里可以传参数[]);export const MyView = () => {const ref = useRef(null);useEffect(() => {// 获取 viewIdconst viewId = findNodeHandle(ref.current);}, []);return (<MyViewManagerref={ref}/>);
};

在官方文档的最后,写了一个例子,介绍如何在 RN 中嵌入 Fragment ,但是我感觉这种场景很少用到,这里就不细说了,感兴趣的同学可以去看看官方文档。

RN 原生模块及组件入门相关推荐

  1. 推荐用于学习RN原生模块开发的开源库—react-native-ble-manager

    如题RN的原生模块/Native Modules的开发是一项很重要的技能,但RN官网的示例又比较简单,然后最近我接触与使用.还有阅读了react-native-ble-manager的部份源码,发现里 ...

  2. APICloud 原生模块、H5模块、多端组件使用教程

    使用APICloud平台,可以使用前端技术快速开发iOS.Android App.小程序.Web等多端应用.在开发过程中,调用模块可以极大的提升开发效率.本文分为三个部分,告诉大家如何使用APIClo ...

  3. Linux内核品读 /基础组件/ 模块机制快速入门

    哈喽,我是杰克吴,继续记录我的学习心得. 一.关于兴趣的几点思考 1. 享受不是兴趣,愿意付出才是: 兴趣很容易跟享受混淆.享受是被动的,无需付出:而兴趣则要求你甘愿为了这件事情付出努力. 2.任何事 ...

  4. React Native调用原生模块

    概述 有时候App需要访问平台API,但React Native可能还没有相应的模块包装:或者你需要复用一些Java代码,而不是用Javascript重新实现一遍:又或者你需要实现某些高性能的.多线程 ...

  5. react-native调用Android原生模块

    今天学习了一下在react-native中调用原生安卓模块的使用,发现很多网上的文章都是直接照抄的文档,这样会有一些坑,导致最后无法运行或者成功调用,所以写下这个博客来分享,同时也记录一下学习过程,内 ...

  6. 【稀饭】react native 实战系列教程之自定义原生模块

    影片详情开发也是通过Cheerio抓取并分析网页获取到的详情数据,本节就不作为详细内容来讲解了,详细的代码可以看下我的github,效果如下: 在点击播放时,会跳转到播放界面,并且横屏显示,退出播放界 ...

  7. Uniapp零基础开发学习笔记(5) -组件入门及容器组件使用

    Uniapp零基础开发学习笔记(5) -组件入门及容器组件使用 按照官网教程学习使用组件,并且重点把容器组件的应用进行了练习. 1.官网关于组件的介绍 组件是视图层的基本组成单元,是一个单独且可复用的 ...

  8. react native 原生模块桥接的简单说明

    原文出自:https://github.com/prscX/awes... 博客链接:https://ssshooter.com/2019-02... Android 创建原生模块包 通过继承 Rea ...

  9. 原生的html组件,如何创建HTML5与原生UI组件混合的移动应用程序

    本文将介绍如何使用Trigger.io创建原生的顶部栏.标签栏.以及HTML/CSS/JavaScript的混合型移动应用程序. 以后我们将添加更多的原生UI组件到Trigger.io,但现在你只需要 ...

最新文章

  1. BFS:图的最短路径  Aizu - 0558 ​​​​​​​Cheese
  2. ModuleNotFoundError: No module named ‘pandas.rpy‘
  3. pip卸载模块/宏包(python)
  4. 克莱姆V(克莱姆相关系数、克莱姆关联系数、独立系数)
  5. linux怎样优化桌面,简单优化设置Ubuntu 18.04系统的GNOME桌面
  6. 小学生python-小学生都在学python
  7. pgpool-II中间件
  8. Robotframework集成jenkins执行用例
  9. button url图片显示不出来_哼!Vue如何在图片上传前使用vue-cropper进行剪切
  10. linux程序打包安装,制作Linux下程序安裝包——使用腳本打包bin、run等安裝包
  11. [kuangbin]专题12 基础DP
  12. 浏览器九宫格的简单实现 - 蒋宇捷的专栏 - 博客频道 - CSDN.NET
  13. java 动态树_使用dtree构建动态树型菜单
  14. jQuery-瀑布流-绝对定位布局(二)(延迟AJAX加载图片)
  15. java基础的第二轮快速学习!day10
  16. Win11如何获得最佳电源效率?
  17. 【集训队互测 2012】Middle
  18. 申论高分作者心得分享——站在政府的角度写申论
  19. 51单片机农历转换公历c语言算法,51单片机阳历转农历(仅仅是阳历转阴历例程)...
  20. 计算字符串相似度算法——Levenshtein

热门文章

  1. USB2.0接口CM4_5G mini驱动板搭配广和通FM150在Win11下的测速
  2. 关于单片机替代PLC的思考
  3. 离职无须迁集体户口 新生儿也能落集体户口
  4. 网页无法显示GIF动画图片解决办法
  5. 一键切换固定IP地址和自动分配IP地址的脚本
  6. 传雅虎秘密搜索用户电邮为美国安全官员提供信息
  7. Verizon 宣布 48 亿美元收购雅虎核心业务
  8. 利用Windows命令行解压zip压缩文件(不借助第三方软件)
  9. light Mode:real-time\mixed\Baked
  10. 浏览器主页被劫持的解决办法、浏览器劫持是什么意思