跳至主内容
版本:0.77

iOS 原生模块

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

信息

原生模块(Native Module)和原生组件(Native Components)是旧架构中使用的稳定技术。它们将在新架构稳定后被弃用。新架构采用 Turbo 原生模块Fabric 原生组件 来实现类似功能。

欢迎了解 iOS 原生模块开发。建议您先阅读 原生模块简介 了解基本概念。

创建日历原生模块

在本指南中,您将创建一个名为 CalendarModule 的原生模块,用于在 JavaScript 中访问苹果的日历 API。最终您将能通过 JavaScript 调用 CalendarModule.createCalendarEvent('Dinner Party', 'My House'); 来触发创建日历事件的本地方法。

环境准备

首先在 Xcode 中打开 React Native 应用的 iOS 项目。React Native 应用的 iOS 项目位于以下路径:

Image of opening up an iOS project within a React Native app inside of Xcode.
Image of where you can find your iOS project

推荐使用 Xcode 编写原生代码。Xcode 专为 iOS 开发设计,能帮助您快速修复代码语法等基础错误。

创建自定义原生模块文件

第一步是创建自定义原生模块的头文件和实现文件。新建名为 RCTCalendarModule.h 的文件

Image of creating a class called  RCTCalendarModule.h.
Image of creating a custom native module file within the same folder as AppDelegate

并添加以下内容:

objectivec
//  RCTCalendarModule.h
#import <React/RCTBridgeModule.h>
@interface RCTCalendarModule : NSObject <RCTBridgeModule>
@end

您可以使用任何符合模块功能的名称。由于创建的是日历模块,我们将类命名为 RCTCalendarModule。因为 Objective-C 没有类似 Java/C++ 的语言级命名空间支持,惯例是在类名前添加前缀(如应用名称缩写)。此处的 RCT 代表 React。

如下所示,CalendarModule 类遵循了 RCTBridgeModule 协议。原生模块本质就是实现 RCTBridgeModule 协议的 Objective-C 类。

接下来开始实现模块。在 Xcode 中使用 Cocoa Touch Class 创建对应的实现文件 RCTCalendarModule.m(与头文件同目录),并添加以下内容:

objectivec
// RCTCalendarModule.m
#import "RCTCalendarModule.h"

@implementation RCTCalendarModule

// To export a module named RCTCalendarModule
RCT_EXPORT_MODULE();

@end

模块名称

当前您的 RCTCalendarModule.m 仅包含 RCT_EXPORT_MODULE 宏,该宏用于向 React Native 注册并导出原生模块类。RCT_EXPORT_MODULE 宏还支持可选参数,用于指定模块在 JavaScript 中的访问名称。

注意该参数不是字符串字面量。例如下方应写为 RCT_EXPORT_MODULE(CalendarModuleFoo) 而非 RCT_EXPORT_MODULE("CalendarModuleFoo")

objectivec
// To export a module named CalendarModuleFoo
RCT_EXPORT_MODULE(CalendarModuleFoo);

这样 JavaScript 中就可以这样访问原生模块:

tsx
const {CalendarModuleFoo} = ReactNative.NativeModules;

若不指定名称,JavaScript 模块名将自动匹配 Objective-C 类名(去除 "RCT" 或 "RK" 前缀)。

我们参考以下示例,调用不带参数的 RCT_EXPORT_MODULE。由于 Objective-C 类名为 CalendarModule(已去除 RCT 前缀),该模块将在 React Native 中公开为 CalendarModule

objectivec
// Without passing in a name this will export the native module name as the Objective-C class name with “RCT” removed
RCT_EXPORT_MODULE();

这样 JavaScript 中就可以这样访问原生模块:

tsx
const {CalendarModule} = ReactNative.NativeModules;

向 JavaScript 暴露原生方法

除非显式声明,否则 React Native 不会暴露原生模块中的任何方法给 JavaScript。这需要通过 RCT_EXPORT_METHOD 宏实现。使用 RCT_EXPORT_METHOD 宏声明的方法均为异步方法,返回类型始终为 void。若需将 RCT_EXPORT_METHOD 方法的结果传回 JavaScript,可使用回调或事件机制(稍后介绍)。现在让我们使用 RCT_EXPORT_METHOD 宏为 CalendarModule 添加 createCalendarEvent() 原生方法,暂时接收 name 和 location 两个字符串参数(参数类型将在后续说明)。

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
}

请注意:除非方法依赖 RCT 参数转换(见下文参数类型),否则 TurboModules 不再需要 RCT_EXPORT_METHOD 宏。React Native 最终将移除 RCT_EXPORT_MACRO,,因此建议避免使用 RCTConvert,改为在方法体内自行处理参数转换。

在构建 createCalendarEvent() 方法的具体功能前,请在方法中添加控制台日志以便确认该方法已从 React Native 应用的 JavaScript 端调用。使用 React 提供的 RCTLog API。首先在文件顶部导入该头文件,然后添加日志调用。

objectivec
#import <React/RCTLog.h>
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

同步方法

您可以使用 RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD 宏创建同步原生方法。

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

该方法的返回类型必须是对象类型 (id) 且可序列化为 JSON。这意味着该方法只能返回 nil 或 JSON 兼容值(如 NSNumber、NSString、NSArray、NSDictionary)。

目前我们不推荐使用同步方法,因为同步调用可能导致严重的性能损耗并引入线程相关错误。此外请注意,如果使用 RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD,应用将无法使用 Google Chrome 调试器——因为同步方法要求 JS 虚拟机与应用共享内存,而 Chrome 调试器中 React Native 运行在浏览器端的 JS 虚拟机,通过 WebSockets 与移动设备异步通信。

测试构建结果

至此您已在 iOS 中搭建好原生模块的基础框架。现在请通过 JavaScript 访问原生模块并调用其导出方法进行测试。

在应用中选定调用原生模块 createCalendarEvent() 的位置。以下示例组件 NewModuleButton 可添加到应用中,在 NewModuleButtononPress() 函数内调用原生模块:

tsx
import React from 'react';
import {Button} from 'react-native';

const NewModuleButton = () => {
const onPress = () => {
console.log('We will invoke the native module here!');
};

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

export default NewModuleButton;

要从 JavaScript 访问原生模块,首先需要从 React Native 导入 NativeModules

tsx
import {NativeModules} from 'react-native';

然后即可通过 NativeModules 访问 CalendarModule 原生模块。

tsx
const {CalendarModule} = NativeModules;

现在 CalendarModule 原生模块已可用,您可以直接调用原生方法 createCalendarEvent()。以下示例将其添加到 NewModuleButtononPress() 方法中:

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

最后一步是重新构建 React Native 应用,使最新原生代码(包含新建的原生模块)生效。在 React Native 应用所在目录执行以下命令:

shell
npm run ios

迭代开发时的构建

迭代开发时的构建

在根据指南开发和迭代原生模块时,您需要重新构建应用的原生部分以获取 JavaScript 端的最新更改。这是因为您编写的代码位于应用的原生部分,而 React Native 的 Metro 打包工具只能监视 JavaScript 变更并实时重建 JS 包,不会处理原生代码变更。因此要测试最新原生修改,必须使用上述命令重新构建。

回顾 ✨

现在您应能在 JavaScript 中调用原生模块的 createCalendarEvent() 方法。由于方法中使用了 RCTLog,您可通过启用应用调试模式在 Chrome 的 JS 控制台或移动端调试器 Flipper 中查看日志,确认原生方法是否被调用。每次调用时都应看到 RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); 消息。

Image of logs.
Image of iOS logs in Flipper

至此您已创建 iOS 原生模块并在 React Native 应用中从 JavaScript 调用其方法。接下来可继续了解原生模块方法的参数类型,以及如何设置回调和 Promise 等进阶内容。

日历模块的进阶应用

优化模块导出方式

通过 NativeModules 导入原生模块的方式略显繁琐。

为了避免原生模块的使用者每次访问时都需要重复操作,你可以为模块创建一个 JavaScript 封装。新建名为 NativeCalendarModule.js 的 JavaScript 文件,内容如下:

tsx
/**
* 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;
export default CalendarModule;

这个 JavaScript 文件也是添加 JavaScript 端功能的好地方。例如,如果你使用 TypeScript 等类型系统,可以在此处为原生模块添加类型注解。虽然 React Native 尚未支持 Native 到 JS 的类型安全,但这些类型注解能让你的 JS 代码保持类型安全。这些注解还能让你未来更容易迁移到类型安全的原生模块。以下是为日历模块添加类型安全的示例:

tsx
/**
* 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;

在其他 JavaScript 文件中可如此访问原生模块并调用其方法:

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

请注意,这里假设你导入 CalendarModule 的位置与 NativeCalendarModule.js 在同一层级。请根据需要更新相对导入。

参数类型

当在 JavaScript 中调用原生模块方法时,React Native 会将参数从 JS 对象转换为对应的 Objective-C/Swift 对象。例如,如果你的 Objective-C 原生模块方法接受 NSNumber 参数,在 JS 中需要用数字调用该方法。React Native 会自动处理类型转换。以下是原生模块方法支持的参数类型及其对应的 JavaScript 类型:

Objective-CJavaScript
NSStringstring, ?string
BOOLboolean
doublenumber
NSNumber?number
NSArrayArray, ?Array
NSDictionaryObject, ?Object
RCTResponseSenderBlockFunction (success)
RCTResponseSenderBlock, RCTResponseErrorBlockFunction (failure)
RCTPromiseResolveBlock, RCTPromiseRejectBlockPromise

以下类型目前受支持,但在 TurboModules 中将不再支持。请避免使用:

  • Function (failure) -> RCTResponseErrorBlock
  • Number -> NSInteger
  • Number -> CGFloat
  • Number -> float

在 iOS 中,你还可以使用 RCTConvert 类支持的任何参数类型编写原生模块方法(详见 RCTConvert)。RCTConvert 辅助函数都接受 JSON 值作为输入,并将其映射到原生 Objective-C 类型或类。

导出常量

原生模块可以通过重写 constantsToExport() 方法导出常量。下面重写了 constantsToExport(),返回包含默认事件名称属性的字典,在 JavaScript 中可这样访问:

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

然后可以通过在 JS 中调用原生模块上的 getConstants() 来访问常量,如下所示:

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

从技术上讲,可以直接通过 NativeModule 对象访问 constantsToExport() 导出的常量。在 TurboModules 中将不再支持此方式,因此我们鼓励社区切换到上述方法,以避免将来不必要的迁移。

请注意,常量仅在初始化时导出,因此如果在运行时更改 constantsToExport() 的值,它不会影响 JavaScript 环境。

对于 iOS,如果你重写了 constantsToExport(),那么你也应该实现 + requiresMainQueueSetup,以便让 React Native 知道你的模块是否需要在主线程上初始化(在任何 JavaScript 代码执行之前)。否则,你会看到一个警告,提示在未来你的模块可能会在后台线程上初始化,除非你使用 + requiresMainQueueSetup: 明确选择退出。如果你的模块不需要访问 UIKit,那么你应该对 + requiresMainQueueSetup 响应 NO。

回调函数

回调函数

对于 iOS,回调函数通过类型 RCTResponseSenderBlock 实现。在下面,回调参数 myCallBack 被添加到 createCalendarEventMethod() 中。

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title
location:(NSString *)location
myCallback:(RCTResponseSenderBlock)callback)

然后,你可以在原生函数中调用回调函数,传入一个数组,其中包含你想要传递给 JavaScript 的任意结果。请注意,RCTResponseSenderBlock 只接受一个参数——即传递给 JavaScript 回调函数的参数数组。接下来,你将传回在之前调用中创建的事件的 ID。

需要重点强调:原生函数执行完成后回调不会立即触发——请记住通信是异步进行的。

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

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

随后可在 JavaScript 中通过以下方式访问该方法:

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

原生模块应当仅调用其回调函数一次。但可以存储回调并在稍后调用,这种模式常用于封装需要委托的 iOS API——可参考 RCTAlertManager 示例。若回调从未被调用,会导致内存泄漏。

回调的错误处理有两种方式:第一种遵循 Node 约定,将回调数组的第一个参数视为错误对象。

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

在 JavaScript 中可检查第一个参数判断是否传递了错误:

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

另一种方案是使用两个独立回调:onFailure 和 onSuccess。

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

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

然后在 JavaScript 中为错误和成功响应分别添加回调:

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

若需向 JavaScript 传递类错误对象,请使用 RCTUtils.h. 中的 RCTMakeError。当前这仅会向 JavaScript 传递字典形式的错误结构,但 React Native 计划未来自动生成真实的 JavaScript 错误对象。也可使用 RCTResponseErrorBlock 参数处理错误回调(接受 NSError \* object),请注意 TurboModules 将不支持此参数类型。

Promise

原生模块也可实现 Promise,这在配合 ES2016 的 async/await 语法时能简化 JavaScript 代码。当原生模块方法的最后一个参数是 RCTPromiseResolveBlockRCTPromiseRejectBlock 时,其对应的 JS 方法将返回 Promise 对象。

将上述代码从回调重构为使用 Promise 的示例如下:

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

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

tsx
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);
}
};

向 JavaScript 发送事件

原生模块无需被直接调用即可向 JavaScript 发出事件信号。例如,当原生 iOS 日历应用中的日程提醒即将触发时通知 JavaScript。推荐方案是子类化 RCTEventEmitter,实现 supportedEvents 并调用 sendEventWithName

更新头文件类,导入 RCTEventEmitter 并继承 RCTEventEmitter

objectivec
//  CalendarModule.h

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

@interface CalendarModule : RCTEventEmitter <RCTBridgeModule>
@end

JavaScript 端可通过创建包裹该模块的 NativeEventEmitter 实例来订阅事件:

若在无监听器时发送事件,会收到资源浪费警告。为避免此情况并优化模块负载(如取消上游通知订阅或暂停后台任务),可在 RCTEventEmitter 子类中重写 startObservingstopObserving 方法。

objectivec
@implementation CalendarModule
{
bool hasListeners;
}

// 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}];
}
}

线程处理

除非原生模块提供专属方法队列,否则不应假设调用线程环境。当前若未提供队列,React Native 会为其创建独立的 GCD 队列并在其中调用方法。请注意这是实现细节可能变更。如需显式指定方法队列,请在原生模块中重写 (dispatch_queue_t) methodQueue 方法。例如需使用仅限主线程的 iOS API 时,应通过以下方式声明:

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

同样地,如果某个操作可能需要较长时间完成,原生模块可以指定自己的队列来执行操作。需要强调的是,虽然当前 React Native 会为你的原生模块提供独立的方法队列,但这属于实现细节不应依赖。如果不提供自定义方法队列,未来你的原生模块中的长时间运行操作可能会阻塞其他无关原生模块上的异步调用。例如这里的 RCTAsyncLocalStorage 模块就创建了专用队列,避免 React 队列被潜在的慢速磁盘访问阻塞。

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

指定的 methodQueue 将被模块中的所有方法共享。如果仅某个方法需要长时间运行(或因特殊原因需在不同队列执行),可在方法内部使用 dispatch_async 将该方法代码调度到其他队列,这样就不会影响其他方法。

objectivec
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(@[...]);
});
}

在模块间共享调度队列

methodQueue 方法会在模块初始化时调用一次,之后由 React Native 保留队列引用,因此除非需要在模块内部使用该队列,否则无需自行保留引用。但若要在多个模块间共享同一队列,必须确保为每个模块都保留并返回相同的队列实例。

依赖注入

React Native 会自动创建并初始化所有已注册的原生模块。但你也可以自行创建和初始化模块实例,以便实现依赖注入等需求。

具体实现方式是:创建遵循 RCTBridgeDelegate 协议的类,以该委托实例为参数初始化 RCTBridge,再用初始化好的 bridge 实例初始化 RCTRootView

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

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

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

导出 Swift 代码

由于 Swift 不支持宏,在 React Native 中向 JavaScript 暴露 Swift 原生模块和方法需要额外配置。但其原理与 Objective-C 相同。假设你需要将 CalendarModule 实现为 Swift 类:

swift
// CalendarModule.swift

@objc(CalendarModule)
class CalendarModule: NSObject {

@objc(addEvent:location:date:)
func addEvent(_ name: String, location: String, date: NSNumber) -> Void {
// Date is ready to use!
}

@objc
func constantsToExport() -> [String: Any]! {
return ["someKey": "someValue"]
}

}

关键点:必须使用 @objc 修饰符确保类和函数能正确导出到 Objective-C 运行时

接着创建私有实现文件,向 React Native 注册必要信息:

objectivec
// CalendarModuleBridge.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(CalendarModule, NSObject)

RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)

@end

Swift/Objective-C 混编提示:在 iOS 项目中混合使用两种语言时,需要桥接头文件将 Objective-C 文件暴露给 Swift。若通过 Xcode 的 File>New File 菜单添加 Swift 文件,Xcode 会自动创建此头文件,你需要在其中导入 RCTBridgeModule.h

objectivec
// CalendarModule-Bridging-Header.h
#import <React/RCTBridgeModule.h>

也可使用 RCT_EXTERN_REMAP_MODULE_RCT_EXTERN_REMAP_METHOD 修改导出模块/方法的 JavaScript 名称。详见 RCTBridgeModule

第三方模块开发注意:仅 Xcode 9+ 支持包含 Swift 的静态库。若模块中的 iOS 静态库使用 Swift,主应用项目必须包含 Swift 代码和桥接头文件才能成功构建。若主应用无 Swift 代码,变通方案是添加空 .swift 文件和空桥接头文件。

保留方法名称

invalidate()

原生模块可通过实现 invalidate() 方法遵循 iOS 的 RCTInvalidating 协议。当原生 bridge 失效时(如开发模式热重载),该方法会被调用,请在此执行原生模块的必要清理操作。