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含有的混合NSNumberNSString。对于数组,RCTConvert提供一些可以在方法声明中使用的类型集合,例如NSStringArrayUIColorArray。对于地图,开发人员有责任通过手动调用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示例。如果从未调用回调,则会泄漏一些内存。如果两个onSuccessonFail回调都过去了,你应该只调用其中的一个。

如果您想将类似错误的对象传递给JavaScript,请使用RCTMakeErrorfrom RCTUtils.h。现在,这只是将错误形状的字典传递给JavaScript,但我们希望Error将来自动生成真正的JavaScript 对象。

承诺

原生模块也可以实现承诺,这可以简化您的代码,特别是在使用ES2016的async/await语法时。当桥接本机方法的最后一个参数是RCTPromiseResolveBlockand时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

优化零听众

如果您在没有听众的情况下发布活动而不必要地花费资源,则会收到警告。为了避免这种情况,并优化模块的工作负载(例如通过取消订阅上游通知或暂停后台任务),您可以覆盖startObservingstopObserving在您的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_MODULERCT_EXTERN_REMAP_METHOD更改要导出的模块或方法的JavaScript名称。欲了解更多信息,请参阅RCTBridgeModule