首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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的缩写。

代码语言:javascript
复制
// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end

除了实现RCTBridgeModule协议外,你的类还必须包含RCT_EXPORT_MODULE()宏。这需要一个可选的参数,指定模块可以在JavaScript代码中访问的名称(稍后会详细介绍)。如果您未指定名称,则JavaScript模块名称将与Objective-C类名称匹配。如果Objective-C类名称以RCT开头,则JavaScript模块名称将排除RCT前缀。

代码语言:javascript
复制
// 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()宏完成的:

代码语言:javascript
复制
#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文件中,您可以调用像这样的方法:

代码语言: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日期对象,因此我们需要将日期转换为字符串或数字。我们可以像这样编写我们的本地函数:

代码语言:javascript
复制
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}

或者像这样:

代码语言:javascript
复制
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}

但是通过使用自动类型转换功能,我们可以完全跳过手动转换步骤,只需编写:

代码语言:javascript
复制
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
  // Date is ready to use!
}

然后您可以使用以下任一方法从JavaScript中调用它:

代码语言:javascript
复制
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.getTime()); // passing date as number of milliseconds since Unix epoch

或者

代码语言:javascript
复制
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.toISOString()); // passing date as ISO-8601 string

并且这两个值都将正确转换为本机NSDate。一个不好的值就像一个Array,会产生一个有用的“RedBox”错误信息。

随着CalendarManager.addEvent方法变得越来越复杂,参数的数量将会增加。其中一些可能是可选的。在这种情况下,值得考虑将API稍微改为接受事件属性字典,如下所示:

代码语言:javascript
复制
#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调用它:

代码语言:javascript
复制
CalendarManager.addEvent('Birthday Party', {
  location: '4 Privet Drive, Surrey',
  time: date.getTime(),
  description: '...'
})

注意:关于数组和映射Objective-C不提供关于这些结构中值的类型的任何保证。你的本机模块所期望的字符串数组,但如果JavaScript和包含数字和字符串数组调用你的方法,你会得到一个NSArray含有的混合NSNumberNSString。对于数组,RCTConvert提供一些可以在方法声明中使用的类型集合,例如NSStringArrayUIColorArray。对于地图,开发人员有责任通过手动调用RCTConvert帮助器方法来单独检查值类型。

回调

警告 此部分比其他人更具实验性,因为我们还没有一套关于回调的最佳实践。

本机模块还支持一种特殊的参数 - 回调。在大多数情况下,它用于将函数调用结果提供给JavaScript。

代码语言:javascript
复制
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
  NSArray *events = ...
  callback(@[[NSNull null], events]);
}

RCTResponseSenderBlock只接受一个参数 - 传递给JavaScript回调的参数数组。在这种情况下,我们使用Node的惯例将第一个参数设置为错误对象(通常null在没有错误的情况下),其余的则是函数的结果。

代码语言:javascript
复制
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而不是回调,如下所示:

代码语言:javascript
复制
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异步函数中的关键字来调用它并等待其结果:

代码语言:javascript
复制
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,则应通过以下方式指定:

代码语言:javascript
复制
- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

同样,如果操作可能需要很长时间才能完成,则本机模块不应该阻塞,并且可以指定它自己的队列来运行操作。例如,RCTAsyncLocalStorage模块创建自己的队列,以便React队列不会被阻塞,等待可能较慢的磁盘访问:

代码语言:javascript
复制
- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

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

代码语言:javascript
复制
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来完成此操作。

代码语言:javascript
复制
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直接使用的常量。这对传送静态数据非常有用,否则这些静态数据需要通过网桥进行往返。

代码语言:javascript
复制
- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}

JavaScript可以同步使用该值:

代码语言:javascript
复制
console.log(CalendarManager.firstDayOfTheWeek);

请注意,常量仅在初始化时导出,所以如果constantsToExport在运行时更改值,则不会影响JavaScript环境。

枚举常量

NS_ENUM没有首先扩展RCTConvert ,通过定义的枚举不能用作方法参数。

为了导出以下NS_ENUM定义:

代码语言:javascript
复制
typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
    UIStatusBarAnimationNone,
    UIStatusBarAnimationFade,
    UIStatusBarAnimationSlide,
};

你必须像这样创建一个RCTConvert的类扩展:

代码语言:javascript
复制
@implementation RCTConvert (StatusBarAnimation)
  RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
                                               @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
                                               @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
                      UIStatusBarAnimationNone, integerValue)
@end

然后你可以定义方法并像这样导出你的枚举常量:

代码语言:javascript
复制
- (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

代码语言:javascript
复制
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>

@end
代码语言:javascript
复制
// 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在你的模块周围创建一个新的实例来订阅这些事件。

代码语言:javascript
复制
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子类中。

代码语言:javascript
复制
@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类:

代码语言:javascript
复制
// 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网桥注册所需的信息:

代码语言:javascript
复制
// 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在此头文件中导入。

代码语言:javascript
复制
// CalendarManager-Bridging-Header.h
#import <React/RCTBridgeModule.h>

您还可以使用RCT_EXTERN_REMAP_MODULERCT_EXTERN_REMAP_METHOD更改要导出的模块或方法的JavaScript名称。欲了解更多信息,请参阅RCTBridgeModule

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com