Native Modules
Native Modules
需要本机代码的项目
此页面仅适用于react-native init
使用Create React Native App 制作的或使用此类应用程序弹出的项目。有关弹出的更多信息,请参阅创建React Native App存储库的指南。
有时应用程序需要访问平台API,而React Native还没有相应的模块。也许你想重用一些现有的Objective-C,Swift或C ++代码,而不必在JavaScript中重新实现它,或者编写一些高性能,多线程的代码,例如图像处理,数据库或任何数量的高级扩展。
我们设计了React Native,因此您可以编写真实的本机代码并访问平台的全部功能。这是一个更高级的功能,我们不希望它成为通常开发过程的一部分,但它存在必不可少。如果React Native不支持您需要的本地功能,您应该可以自己构建它。
这是一个更高级的指南,展示了如何构建本地模块。它假定读者知道Objective-C或Swift和核心库(Foundation,UIKit)。
iOS日历模块示例
本指南将使用iOS日历API示例。假设我们希望能够从JavaScript访问iOS日历。
本地模块只是一个实现RCTBridgeModule
协议的Objective-C类。如果您想知道,RCT是ReaCT的缩写。
// CalendarManager.h
#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
除了实现RCTBridgeModule
协议外,你的类还必须包含RCT_EXPORT_MODULE()
宏。这需要一个可选的参数,指定模块可以在JavaScript代码中访问的名称(稍后会详细介绍)。如果您未指定名称,则JavaScript模块名称将与Objective-C类名称匹配。如果Objective-C类名称以RCT开头,则JavaScript模块名称将排除RCT前缀。
// CalendarManager.m
@implementation CalendarManager
// To export a module named CalendarManager
RCT_EXPORT_MODULE(
// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager
@end
React Native不会CalendarManager
向JavaScript 公开任何方法,除非明确告知。这是使用RCT_EXPORT_METHOD()
宏完成的:
#import "CalendarManager.h"
#import <React/RCTLog.h>
@implementation CalendarManager
RCT_EXPORT_MODULE(
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location
}
现在,从您的JavaScript文件中,您可以调用像这样的方法:
import { NativeModules } from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey'
注
:JavaScript方法名称导出到JavaScript的方法的名称是直到第一个冒号的本机方法名称。React Native还定义了一个调用RCT_REMAP_METHOD()
来指定JavaScript方法名称的宏。当多个本地方法相同直到第一个冒号并且会有冲突的JavaScript名称时,这很有用。
CalendarManager模块使用CalendarManager新调用在Objective-C一侧实例化。桥接方法的返回类型始终是void
。React Native桥是异步的,因此将结果传递给JavaScript的唯一方法是使用回调或发射事件(请参见下文)。
参数类型
RCT_EXPORT_METHOD
支持所有标准的JSON对象类型,例如:
- string (
NSString
)
- number (
NSInteger
,float
,double
,CGFloat
,NSNumber
)
- boolean (
BOOL
,NSNumber
)
- array (
NSArray
) of any types from this list
NSDictionary
带有字符串键的对象()以及此列表中任何类型的值
- function (
RCTResponseSenderBlock
)
但它也适用于该类支持的任何类型RCTConvert
(请参阅RCTConvert
详细信息)。该RCTConvert
辅助功能都接受一个JSON值作为输入,并将其映射到一个本地目标C类型或类。
在我们的CalendarManager
例子中,我们需要将事件日期传递给本地方法。我们无法通过网桥发送JavaScript日期对象,因此我们需要将日期转换为字符串或数字。我们可以像这样编写我们的本地函数:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}
或者像这样:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}
但是通过使用自动类型转换功能,我们可以完全跳过手动转换步骤,只需编写:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
// Date is ready to use!
}
然后您可以使用以下任一方法从JavaScript中调用它:
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.getTime() // passing date as number of milliseconds since Unix epoch
或者
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.toISOString() // passing date as ISO-8601 string
并且这两个值都将正确转换为本机NSDate
。一个不好的值就像一个Array
,会产生一个有用的“RedBox”错误信息。
随着CalendarManager.addEvent
方法变得越来越复杂,参数的数量将会增加。其中一些可能是可选的。在这种情况下,值得考虑将API稍微改为接受事件属性字典,如下所示:
#import <React/RCTConvert.h>
RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
NSString *location = [RCTConvert NSString:details[@"location"]];
NSDate *time = [RCTConvert NSDate:details[@"time"]];
...
}
并从JavaScript调用它:
CalendarManager.addEvent('Birthday Party', {
location: '4 Privet Drive, Surrey',
time: date.getTime(),
description: '...'
})
注意
:关于数组和映射Objective-C不提供关于这些结构中值的类型的任何保证。你的本机模块所期望的字符串数组,但如果JavaScript和包含数字和字符串数组调用你的方法,你会得到一个NSArray
含有的混合NSNumber
和NSString
。对于数组,RCTConvert
提供一些可以在方法声明中使用的类型集合,例如NSStringArray
或UIColorArray
。对于地图,开发人员有责任通过手动调用RCTConvert
帮助器方法来单独检查值类型。
回调
警告
此部分比其他人更具实验性,因为我们还没有一套关于回调的最佳实践。
本机模块还支持一种特殊的参数 - 回调。在大多数情况下,它用于将函数调用结果提供给JavaScript。
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]
}
RCTResponseSenderBlock
只接受一个参数 - 传递给JavaScript回调的参数数组。在这种情况下,我们使用Node的惯例将第一个参数设置为错误对象(通常null
在没有错误的情况下),其余的则是函数的结果。
CalendarManager.findEvents((error, events) => {
if (error) {
console.error(error
} else {
this.setState{events: events}
}
})
本地模块应该只调用一次回调函数。可以存储回调并稍后调用它。这种模式通常用于包装需要委托的iOS API - 请参阅RCTAlertManager
示例。如果从未调用回调,则会泄漏一些内存。如果两个onSuccess
和onFail
回调都过去了,你应该只调用其中的一个。
如果您想将类似错误的对象传递给JavaScript,请使用RCTMakeError
from RCTUtils.h
。现在,这只是将错误形状的字典传递给JavaScript,但我们希望Error
将来自动生成真正的JavaScript 对象。
承诺
原生模块也可以实现承诺,这可以简化您的代码,特别是在使用ES2016的async/await
语法时。当桥接本机方法的最后一个参数是RCTPromiseResolveBlock
and时RCTPromiseRejectBlock
,其相应的JS方法将返回一个JS Promise对象。
重构上述代码以使用promise而不是回调,如下所示:
RCT_REMAP_METHOD(findEvents,
findEventsWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events = ...
if (events) {
resolve(events
} else {
NSError *error = ...
reject(@"no_events", @"There were no events", error
}
}
此方法的JavaScript对应方返回Promise。这意味着您可以使用await
异步函数中的关键字来调用它并等待其结果:
async function updateEvents() {
try {
var events = await CalendarManager.findEvents(
this.setState{ events }
} catch (e) {
console.error(e
}
}
updateEvents(
思路
本地模块不应该对它被调用的线程有任何假设。React Native在单独的串行GCD队列上调用本地模块方法,但这是实现细节,可能会更改。该- (dispatch_queue_t)methodQueue
方法允许本地模块指定应该在哪个队列上运行它的方法。例如,如果需要使用仅有主线程的iOS API,则应通过以下方式指定:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue(
}
同样,如果操作可能需要很长时间才能完成,则本机模块不应该阻塞,并且可以指定它自己的队列来运行操作。例如,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(@[...]
}
}
注意
:在模块之间共享调度队列在模块methodQueue
初始化后,方法将被调用一次,然后由网桥保留,所以不需要自己保留队列,除非您希望在模块中使用它。但是,如果您希望在多个模块之间共享相同的队列,则需要确保您保留并为每个模块返回相同的队列实例; 仅仅返回一个相同名字的队列将无法工作。
Dependency Injection
该桥会自动初始化任何已注册的RCTBridgeModules,但您可能希望实例化您自己的模块实例(例如,您可能会注入依赖关系)。
您可以通过创建一个实现RCTBridgeDelegate协议的类,使用委托作为参数初始化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];
导出常量
本地模块可以在运行时导出可供JavaScript直接使用的常量。这对传送静态数据非常有用,否则这些静态数据需要通过网桥进行往返。
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
JavaScript可以同步使用该值:
console.log(CalendarManager.firstDayOfTheWeek
请注意,常量仅在初始化时导出,所以如果constantsToExport
在运行时更改值,则不会影响JavaScript环境。
枚举常量
NS_ENUM
没有首先扩展RCTConvert ,通过定义的枚举不能用作方法参数。
为了导出以下NS_ENUM
定义:
typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
UIStatusBarAnimationNone,
UIStatusBarAnimationFade,
UIStatusBarAnimationSlide,
};
你必须像这样创建一个RCTConvert的类扩展:
@implementation RCTConvert (StatusBarAnimation)
RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
UIStatusBarAnimationNone, integerValue)
@end
然后你可以定义方法并像这样导出你的枚举常量:
- (NSDictionary *)constantsToExport
{
return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
};
RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
completion:(RCTResponseSenderBlock)callback)
然后integerValue
,在传递给导出的方法之前,您的枚举将使用提供的选择器(在上例中)自动解包。
发送事件到JavaScript
本地模块可以向JavaScript发送事件而不用直接调用。执行此操作的首选方法是继承RCTEventEmitter
,实现supportedEvents
和调用self sendEventWithName
:
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>
@end
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
RCT_EXPORT_MODULE(
- (NSArray<NSString *> *)supportedEvents
{
return @[@"EventReminder"];
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *eventName = notification.userInfo[@"name"];
[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)
...
// Don't forget to unsubscribe, typically in componentWillUnmount
subscription.remove(
有关将事件发送到JavaScript的更多示例,请参阅RCTLocationObserver
。
优化零听众
如果您在没有听众的情况下发布活动而不必要地花费资源,则会收到警告。为了避免这种情况,并优化模块的工作负载(例如通过取消订阅上游通知或暂停后台任务),您可以覆盖startObserving
并stopObserving
在您的RCTEventEmitter
子类中。
@implementation CalendarManager
{
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}];
}
}
导出Swift
Swift不支持宏,因此将它暴露给React Native需要更多的设置,但工作原理相同。
假设我们有相同的CalendarManager
但是作为Swift类:
// CalendarManager.swift
@objc(CalendarManager)
class CalendarManager: NSObject {
@objc(addEvent:location:date:)
func addEvent(name: String, location: String, date: NSNumber) -> Void {
// Date is ready to use!
}
override func constantsToExport() -> [String: Any]! {
return ["someKey": "someValue"]
}
}
注意
:使用@objc修饰符来确保将类和函数正确导出到Objective-C运行库非常重要。
然后创建一个私有实现文件,用于向React Native网桥注册所需的信息:
// CalendarManagerBridge.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(CalendarManager, 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在此头文件中导入。
// CalendarManager-Bridging-Header.h
#import <React/RCTBridgeModule.h>
您还可以使用RCT_EXTERN_REMAP_MODULE
并RCT_EXTERN_REMAP_METHOD
更改要导出的模块或方法的JavaScript名称。欲了解更多信息,请参阅RCTBridgeModule
。