iOS 原生模块
原生模块和原生组件是我们传统架构中使用的稳定技术。 当新架构稳定后,它们将被弃用。新架构使用TurboModule和Fabric 组件来实现类似的功能。
有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块封装;或者你需要复用 Objective-C、Swift 或 C++代码,而不是用 JavaScript 重新实现一遍;又或者你需要实现某些高性能、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
我们把 React Native 设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果 React Native 还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。
本文是关于如何封装原生模块的高级向导,我们假设您已经具备 Objective-C 或者 Swift,以及 iOS 核心库(Foundation、UIKit)的相关知识。
Native Module Setup
Native modules are usually distributed as npm packages, except that for them to be native modules they will contain an Xcode library project. To get the basic scaffolding make sure to read Native Modules Setup guide first.
iOS 日历模块演示
本向导将会用iOS 日历 API作为示例。我们的目标就是在 Javascript 中可以访问到 iOS 的日历功能。
在 React Native 中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的 Objective-C 类,其中 RCT 是 ReaCT 的缩写。
// CalendarManager.h
#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
为了实现RCTBridgeModule
协议,你的类需要包含RCT_EXPORT_MODULE()
宏。这个宏也可以添加一个参数用来指定在 JavaScript 中访问这个模块的名字。如果你不指定,默认就会使用这个 Objective-C 类的名字。如果类名以 RCT 开头,则 JavaScript 端引入的模块名会自动移除这个前缀。
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
// To export a module named CalendarManager
RCT_EXPORT_MODULE();
// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager);
@end
你必须明确的声明要给 JavaScript 导出的方法,否则 React Native 不会导出任何方法。声明通过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);
}
@end
现在从 Javascript 里可以这样调用这个方法:
import { NativeModules } from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey'
);
注意: JavaScript 方法名
导出到 JavaScript 的方法名是 Objective-C 的方法名的第一个部分。React Native 还定义了一个
RCT_REMAP_METHOD()
宏,它可以指定 JavaScript 方法名。因为 JavaScript 端不能有同名不同参的方法存在,所以当原生端存在重载方法时,可以使用这个宏来避免在 JavaScript 端的名字冲突。
The CalendarManager module is instantiated on the Objective-C side using a [CalendarManager new] call. 桥接到 JavaScript 的方法返回值类型必须是void
。React Native 的桥接操作是异步的,所以要返回结果给 JavaScript,你必须通过回调或者触发事件来进行。(参见本文档后面的部分)
参数类型
RCT_EXPORT_METHOD
支持所有标准 JSON 类型,包括:
- string (
NSString
) - number (
NSInteger
,float
,double
,CGFloat
,NSNumber
) - boolean (
BOOL
,NSNumber
) - array (
NSArray
) 可包含本列表中任意类型 - object (
NSDictionary
) 可包含 string 类型的键和本列表中任意类型的值 - function (
RCTResponseSenderBlock
)
除此以外,任何RCTConvert
类支持的的类型也都可以使用(参见RCTConvert
了解更多信息)。RCTConvert
还提供了一系列辅助函数,用来接收一个 JSON 值并转换到原生 Objective-C 类型或类。
在我们的CalendarManager
例子里,我们需要把事件的时间交给原生方法。我们不能在桥接通道里传递 Date 对象,所以需要把日期转化成字符串或数字来传递。我们 可以这么实现原生函数:
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()
); // 把日期以unix时间戳形式传递
也可以这样:
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
date.toISOString()
); // 把日期以ISO-8601的字符串形式传递
两个值都会被转换为正确的NSDate
类型。但如果提供一个不合法的值,譬如一个Array
,则会产生一个“红屏”报错信息。
随着CalendarManager.addEvent
方法变得越来越复杂,参数的个数越来越多,其中有一些可能是可选的参数。在这种情况下我们应该考虑修改我们的 API,用一个 dictionary 来存放所有的事件参数,像这样:
#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"]];
...
}
然后在 JS 里这样调用:
CalendarManager.addEvent('Birthday Party', {
location: '4 Privet Drive, Surrey',
time: date.getTime(),
description: '...'
});
注意: 关于数组和映射
Objective-C 并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望收到一个字符串数组,但如果 JavaScript 在调用的时候提供了一个混合 number 和 string 的数组,你会收到一个
NSArray
,里面既有NSNumber
也有NSString
。对于数组来说,RCTConvert
提供了一些类型化的集合,譬如NSStringArray
或者UIColorArray
,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用RCTConvert
的辅助方法来检测和转换值的类型。