原生模块

NativeModules system 将 Java / Objective-C / C++ (原生)类的实例作为 JS 对象暴露给 JS ,因此允许你在 JS 中执行任意的原生代码。

有两种方法可以为 React Native 应用编写原生模块:

  1. 直接在 React Native 应用的 iOS / Android 项目中;

  2. 作为一个 NPM 包,依赖安装在 React Native 应用中。

iOS 原生模块

在下面的指南中,你将创建一个本地模块 CalendarModule ,它将允许你通过 JavaScript 访问 iOS 的日历 API 。最终你可以通过在 JavaScript 中调用 CalendarModule.createCalendarEvent('Dinner Party', 'My House'); 来调用原生的方法创建一个日历事项。

在 Xcode 中创建 Objective-C 类

首先在 Xcode 中创建 RCTCalendarModule 类。由于 Objective-C 没有类似 Java 或 C++ 那样的语言层级的命名空间,因此通过添加前缀来避免重名。在这个例子中,前缀用的是 RCT ,代表 React

RCTCalendarModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface RCTCalendarModule : NSObject <RCTBridgeModule>

@end

RCTCalendarModule.m

#import "RCTCalendarModule.h"
#import <React/RCTLog.h>

@implementation RCTCalendarModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
    RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
    NSLog(@"This is NSLog!");
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getName)
{
    return [[UIDevice currentDevice] name];
}

@end

模块的名称

RCTCalendarModule 类遵守了 RCTBridgeModule 协议。原生模块 (native module) 的本质就是实现了 RCTBridgeModule 协议的 Objective-C 类。

使用 RCT_EXPORT_MODULE 宏来导出和注册原生模块,它有个可选参数,用来指定在 JavaScript 代码中访问该模块时使用的名称。请注意这个参数不是字符串字面量,使用 RCT_EXPORT_MODULE(CalendarModuleFoo)不是 RCT_EXPORT_MODULE("CalendarModuleFoo")

如果未指定该参数,则在 JavaScript 中默认使用不带前缀的 Objective-C 的类名。比如,RCTCalendarModule 在 JavaScript 中使用 CalendarModule 来访问。

在 JS 中访问上述原生模块,需要先导入 NativeModules

import { NativeModules } from 'react-native';

使用 CalendarModule

const { CalendarModule } = ReactNative.NativeModules;

原生方法导出

异步方法:RCT_EXPORT_METHOD

使用 RCT_EXPORT_METHOD 宏导出的方法是异步的,因此其返回值类型总是 void 。为了将 RCT_EXPORT_METHOD 导出的方法的结果传递给 JavaScript ,你可以使用 callback 或发送通知。

在上面的例子中创建了一个名为 createCalendarEvent 的方法,带有两个名为 namelocationNSString 参数。在这个方法中调用了 RCTLog ,稍后在 JS 代码中调用 createCalendarEvent 方法,就可以在 Xcode 或 Chrome Debugger 或 Flipper (for Mac) 中查看的 log 。

Please note that the RCT_EXPORT_METHOD macro will not be necessary with TurboModules unless your method relies on RCT argument conversion (see argument types below). Ultimately, React Native will remove RCT_EXPORT_MACRO, so we discourage people from using RCTConvert. Instead, you can do the argument conversion within the method body.

同步方法:RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD

你可以使用 RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD 来创建一个原生的同步方法:

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getName)
{
    return [[UIDevice currentDevice] name];
}

这个方法的返回值类型必须是对象类型 id 并且可被序列化为 JSON 。这意味着只能返回 nil 或可转成 JSON 的值(如 NSNumber, NSString, NSArray, NSDictionary )。

目前,我们不推荐使用同步方法,因为同步调用方法可能会有很大的性能损失,并且会给本地模块带来线程相关的 bug 。另外,请注意,如果选择使用 RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD ,App 将不能使用 Chrome debugger 。这是因为同步方法需要 JS VM 与 App 共享内存。对于 Chrome debugger ,React Native 运行在 Chrome 的 JS VM 中,并通过 WebSocket 与移动设备异步通信

调用原生模块的示例

import React from 'react';
import { NativeModules, Button, SafeAreaView } from 'react-native';

const { CalendarModule } = NativeModules;

const NewModuleButton = () => {
  const onPress = () => {
    CalendarModule.createCalendarEvent('testName', 'testLocation');
  };

  return (
    <SafeAreaView>
      <Button
        title="Click to invoke your native module!"
        color="#841584"
        onPress={onPress}
      />
    </SafeAreaView>

  );
};

export default NewModuleButton;

更好地导出原生模块

像上面那样导出 NativeModules 模块有一点笨重。为了避免每次在 RN 中使用原生模块时做重复的事情,可以为原生模块创建一个 JavaScript 包装器。

创建一个名为 NativeCalendarModule.js 的 JavaScript 文件,添加下述内容:

import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules;
export default CalendarModule;

如果你使用 TypeScript ,在这个文件中也可以很方便地为这个原生模块添加 type annotations

import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules
interface CalendarInterface {
   createCalendarEvent(name: string, location: string): void;
}
export default CalendarModule as CalendarInterface;

在其他 JavaScript 文件中,只需要导入上述模块即可调用其包装的原生模块:

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

参数类型

https://reactnative.dev/docs/next/native-modules-ios#argument-types

导出常量

原生模块可以通过重写原生方法 -constantsToExport 来导出常量。如:

- (NSDictionary *)constantsToExport
{
    return @{ @"DEFAULT_EVENT_NAME": @"New Event" };
}

在 JS 中可通过在原生模块上调用 getConstants() 来访问上面导出的常量:

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

Technically, it is possible to access constants exported in -constantsToExport directly off the NativeModule object. This will no longer be supported with TurboModules, so we encourage the community to switch to the above approach to avoid necessary migration down the line.

注意:常量是在初始化的时候导出的,如果你在运行时修改了 -constantsToExport 方法中的值,对 JavaScript 环境是不生效的。

对于 iOS 项目,如果重写了 -constantsToExport ,则还需要重写 +requiresMainQueueSetup: 方法来让 React Native 知道你的模块是否需要在主线程上初始化(在任何 JavaScript 代码执行之前)。否则,你会看到一个警告:在未来,你的模块可能会在后台线程上初始化,除非你明确选择使用 +requiresMainQueueSetup: 来指定在主线程执行。如果你的模块不需要访问 UIKit ,那么你应该在 + requiresMainQueueSetup: 中返回 NO

Callbacks

callback 在异步方法中用于把数据从 Objective-C 传递到 JavaScript 。它们还可以用于在原生代码中异步执行 JS 。

对于 iOS ,callback 是使用 RCTResponseSenderBlock 类型实现的。注意,RCTResponseSenderBlock 只接受一个数组类型的参数。如:

RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title
                  location:(NSString *)location
                  callback:(RCTResponseSenderBlock)callback)
{
    NSInteger eventId = 101;
    callback(@[@(eventId)]);

    RCTLogInfo(@"[Xcode]Pretending to create an event %@ at %@", title, location);
}

在 JavaScript 中调用上述方法,第三个参数就是回调:

const onPress = () => {
  NativeCalendarModule.createCalendarEvent(
    'Party',
    '04-12-2020',
    (eventId) => {
      console.log(`Created a new event with id ${eventId}`);
    }
  );

原生模块应该只调用一次它的回调函数。但是,它可以存储回调,并在稍后调用它。这个模式通常用于包装需要委托的 iOS API — 参见 RCTAlertManager 的示例。如果回调从未调用,则会泄漏一些内存。

有两种方法处理包含错误信息的回调。第一种方法是遵循 Node 的约定,将传递给回调数组(即 callback 唯一的数组类型的参数)的第一个参数视为错误对象:

RCT_EXPORT_METHOD(createCalendarEventCallback:(NSString *)title
                  location:(NSString *)location
                  callback:(RCTResponseSenderBlock)callback)
{
    NSNumber *eventId = [NSNumber numberWithInt:123];
    callback(@[[NSNull null], eventId]);
}

在 JavaScript 中,你可以检查第一个参数来判断是否有 error :

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

第二种可选的方法是使用两个独立的 callback ,onFailureonSuccess

RCT_EXPORT_METHOD(createCalendarEventCallback:(NSString *)title
                  location:(NSString *)location
                  errorCallback:(RCTResponseSenderBlock)errorCallback
                  successCallback:(RCTResponseSenderBlock)successCallback)
{
  @try {
    NSNumber *eventId = [NSNumber numberWithInt:456];
    successCallback(@[eventId]);
  }

  @catch ( NSException *e ) {
    errorCallback(@[e]);
  }
}

然后在 JavaScript 中,你可以为错误和成功的响应分别添加一个的回调:

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

If you want to pass error-like objects to JavaScript, use RCTMakeError from RCTUtils.h. Right now this only passes an Error-shaped dictionary to JavaScript, but React Native aims to automatically generate real JavaScript Error objects in the future. You can also provide a RCTResponseErrorBlock argument, which is used for error callbacks and accepts an NSError \* object. Please note that this argument type will not be supported with TurboModules.

Promise

原生模块也可以实现一个 Promise ,这可以简化你的 JavaScript ,特别是当你使用 ES2016async/await 语法时。当一个原生模块方法的最后一个参数是 RCTPromiseResolveBlockRCTPromiseRejectBlock 时,它对应的 JS 方法将返回一个 JS Promise 对象。如:

RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title
                 location:(NSString *)location
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
//    NSInteger eventId = createCalendarEvent();
    NSInteger eventId = 789;
    if (eventId) {
        resolve(@(eventId));
    } else {
        reject(@"event_failure", @"no event id returned", nil);
    }
}

这个方法对应的 JavaScript 返回一个 Promise 。这意味着你可以在 async 函数中使用 await 关键字来调用它并等待它的结果:

const onPress = async () => {
  try {
    const eventId = await NativeCalendarModule.createCalendarEvent(
      'Party',
      'my house'
    );
    console.log(`Created a new event with id ${eventId}`);
  } catch (e) {
    console.error(e);
  }
};

发送事件到 JavaScript

原生模块可以向 JavaScript 发送事件通知,而无需直接调用。例如,你可能希望向 JavaScript 发出通知,提醒本地 iOS 日历应用中将很快出现一个日历事件。首选的方法是创建 RCTEventEmitter 的子类,实现 supportedEvents 方法并调用 [self sendEventWithName:body:]

添加 RCTEventEmitter 的子类 CalendarManager

//  CalendarManager.h

#import <React/RCTEventEmitter.h>

@interface CalendarManager : RCTEventEmitter
@end

如果你在没有监听器的情况下发出通知而消耗了不必要的资源,则会收到警告。为了避免这种情况,并优化模块的工作负载(例如通过取消订阅上游通知或暂停后台任务),你可以在 RCTEventEmitter 子类中重写 startObservingstopObserving 方法。

@implementation CalendarManager
{
  BOOL hasListeners;
}

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents {
    return @[@"EventReminder"];
}

// Will be called when this module's first listener is added.
- (void)startObserving {
    hasListeners = YES;
    // Set up any upstream listeners or background tasks as necessary
}

// Will be called when this module's last listener is removed, or on dealloc.
- (void)stopObserving {
    hasListeners = NO;
    // Remove upstream listeners, stop unnecessary background tasks
}

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  if (hasListeners) { // Only send events if anyone is listening
    [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
  }
}
@end

JavaScript 端的代码可以创建一个包含你的模块的 NativeEventEmitter 实例来订阅这些事件。

import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;

const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);

const subscription = calendarManagerEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
// ...
// 别忘了取消订阅,通常在 componentWillUnmount 生命周期方法中实现。
subscription.remove();

多线程

除非原生模块提供了自己的方法队列,否则它不应该对调用它的线程做任何假设。目前,如果原生模块不提供方法队列,React Native 将为它创建一个单独的 GCD 队列,并在那里调用它的方法。请注意,这是一个实现细节,可能会更改。

如果您想显式地为原生模块提供方法队列,请重写原生模块中的 - (dispatch_queue_t)methodQueue 方法。例如,如果它需要使用一个只能在主线程执行的 iOS API,它应该通过如下方式指定:

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

类似地,如果某个操作可能需要很长时间才能完成,原生模块可以指定它自己的队列去执行。同样,React Native 目前将为你的原生模块提供一个单独的方法队列,但这是一个你不应该依赖的实现细节。如果你不提供你自己的方法队列,在将来,你的原生模块的长时间运行的操作可能会阻塞其他不相关的原生模块上执行的异步调用。例如,RCTAsyncLocalStorage 模块创建了自己的队列,这样 React 队列就不会在等待可能很慢的磁盘访问时被阻塞。

- (dispatch_queue_t)methodQueue
{
 return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

指定的 methodQueue 将被模块中的所有方法共享。如果你的方法中只有一个是长时间运行的(或由于某些原因需要在不同的队列上运行),你可以在方法内部使用 dispatch_async 在另一个队列上执行特定方法的代码,而不影响其他方法:

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   // Call long-running code on background thread
   ...
   // You can invoke callback from any thread/queue
   callback(@[...]);
 });
}

Sharing dispatch queues between modules The methodQueue method will be called once when the module is initialized, and then retained by React Native, so there is no need to keep a reference to the queue yourself, unless you wish to make use of it within your module. However, if you wish to share the same queue between multiple modules then you will need to ensure that you retain and return the same queue instance for each of them.

依赖注入 (Dependency Injection)

bridge 会自动注册实现了 RCTBridgeModule 协议的模块,但是你可能也希望能够自己去初始化自定义的模块实例,这样可以注入依赖 (inject dependencies)

要实现这个功能,你需要实现 RCTBridgeDelegate 协议,初始化 RCTBridge ,并且在初始化方法里指定代理。然后用初始化好的 RCTBridge 实例初始化一个 RCTRootView

id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];

RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];

RCTRootView *rootView = [[RCTRootView alloc]
                        initWithBridge:bridge
                            moduleName:kModuleName
                     initialProperties:nil];

Exporting Swift

https://reactnative.dev/docs/native-modules-ios#exporting-swift

Reserved Method Names

invalidate

原生模块可以通过实现 - (void)invalidate 方法来遵守 iOS 上的 RCTInvalidating 协议。这个方法可以在 native bridge 失效时调用(例如:在 dev mode 中执行 reload 时)。请在必要时使用此机制对本机模块进行必要的清理。

Native Modules NPM Package Setup

NPM 文档:Contributing packages to the registry

为了建立原生模块的基本项目结构,我们将使用社区工具 create-react-native-library

npx create-react-native-library react-native-awesome-module

交互式配置:

npx: installed 81 in 9.109s
✔ What is the name of the npm package? … react-native-demo-module
✔ What is the description for the package? … rn demo module
✔ What is the name of package author? … Huang-Libo
✔ What is the email address for the package author? … LiboHwang+IT@gmail.com
✔ What is the URL for the package author? … https://github.com/Huang-Libo
✔ What is the URL for the repository? … https://github.com/Bob-Playground/react-native-demo-module
✔ Which languages do you want to use? › Java & Objective-C
✔ What type of library do you want to develop? › Native module (to expose native APIs)

执行成功的输出:

Project created successfully at react-native-awesome-module!

Get started with the project:

  $ yarn

Run the example app on iOS:

  $ yarn example ios

Run the example app on Android:

  $ yarn example android

Good luck!

运行 iOS 项目时报错:

react-native-awesome-module/example/ios/Pods/Headers/Public/Flipper-Folly/folly/portability/Time.h:52:17: 
Typedef redefinition with different types ('uint8_t' (aka 'unsigned char') vs 'enum clockid_t')

原因是 Flipper-Folly 库中有个宏的 __IPHONE_10_0 应该改为 __IPHONE_12_0 ,但直接改源码不太合适,可以在 Podfile 的 post_install 中添加(参考 flipper issue 的评论 ):

post_install do |installer|
  flipper_post_install(installer)
+  `sed -i -e  $'s/__IPHONE_10_0/__IPHONE_12_0/' Pods/Flipper-Folly/folly/portability/Time.h`
end

再次执行 pod install 后,Flipper-Folly 源码中有问题的宏就被更正了。

另一个创建 module 的第三方工具:create-react-native-module

Last updated

Was this helpful?