ReactNative iOS原生模块

ReactNative iOS原生模块

七月 10, 2021

前言:

因为工作需要,开始接触React Native和iOS开发,所以最近打算学习一下。基础的部分相对简单,就不过多阐述,这部分主要学习一下最核心的部分,原生模块。


iOS原生模块

有时候APP需要访问平台API,但React Native可能没有对应的封装;有时候可能需要对Object- C、Swift或C++代码进行复用,而不是用JavaScript重复实现;有时候可能需要实现一些JavaScript无法实现的高性能、多线程代码,例如图片处理、数据库或者各种高级扩展。

这个时候我们就需要对平台的原生模块进行封装。这里通过对iOS平台的日历模块进行封装,来学习React Native如何实现对iOS原生模块的封装。

iOS日历模块演示

在React Native中,一个原生模块就是一个实现了RCTBridgeModule协议的Objective-C类,其中RCT是ReaCT的缩写。首先在项目/ios/项目名称文件夹下创建CalendarManager.h文件。.h文件是Objective- C中的头文件,它包含类名,类继承的父类,还有方法和变量的声明。它定义的类的成员变量和方法是公开的。然后在文件中定义一个继承RCTBridgeModule类的CalendarManager

1
2
3
4
5
// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end

接下来,为了具体实现RCTBridgeModule协议,我们需要在同路径下创建一个CalendarManager.m文件。.m文件是对.h文件中方法的具体实现文件,可以包含Objective- C和C代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "CalendarManager.h"
#import <React/RCTLog.h>

@implementation CalendarManager

RCT_EXPORT_MODULE();

// RCT_EXPORT_MODULE(AwesomeCalendarManager);

RCT_EXPORT_METHOD(addEvent:(NSSTring *)name location:(NSString *)location) {
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

@end

在这段代码中,定义了两个宏:

  • RCT_EXPORT_MODULE()宏:这个宏可以添加一个参数用来制定在Java Script中访问这个模块的名字。如果不指定这个参数,默认会使用Objective-C类的名字。如果类名以RCT开头,则JavaScript端引入的模块名会自动移除这个前缀。
  • RCT_EXPORT_METHOD()宏:这个宏用于声明要导出给JavaScript使用的方法

Objective-C的函数参数:

Objective-C函数参数的定义形式有些奇特,它在定义单个函数参数和多个函数参数的时候的形式是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 单函数参数声明
-(void)call:(int *)array;
// 对应的C语言形式
void call(int *array);

// 多函数参数的OC方法,需要声明除第一个参数外其他参数的别称
-(void)call:(int *)array array_size:(int)size;
// 对应的C++函数
void call(int *arrya, int size);

/*
array_size是参数size的别称 是整个call方法 方法名的一部分
在oc方法实体中,引用的第二个参数名是size,而不是array_size
而调用oc方法call时,应该指定参数名array_size而不是size
*/

备注:为什么第一个参数不需要别名的问题还需解决

现在从JavaScript里可以这样调用这个方法

1
2
3
4
5
6
import { NativeModules } from 'react-natvie';
const Calendarmanager = NativeModules.CalendarManager;
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
)

注意:JavaScript方法名

导出到JavaScript的方法名是Objective-C的方法名的一个部分。React Native还定义了一个RCT_REMAP_METHOD()宏,它可以指定JavaScript方法名。具体用法如下

1
2
3
RCT_REMAP_METHOD(方法名, 参数描述) {
// 方法具体执行代码
}

因为JavaScript端不能有同名不同参的方法存在,所以当原生端存在重载方法时,可以使用这个宏来避免在JavaScript端的名字冲突。

注意:RCT_REMAP_METHOD的使用,要注意如果是两个函数名不同,但是参数不一样,会出现报错

桥接到Java Script的返回值类型必须是void。React Native的桥接操作是异步的,所以要返回结果给JavaScript,你必须通过回调或者触发事件来进行。

参数类型

RCT_EXPORT_METHOD支持所有标准JSON类型,包括:

  • String:NSString
  • Number:NSInteger,float,doubler,CGFloat,NSNumber
  • Boolean:BOOL,NSNumber
  • Array:NSArray,可以为本列表中任意类型数组
  • Object:NSDictionary,可包含String类型的键和本列表中任意类型的值
  • Function:RCTResponseSenderBlock

除此之外,任何RCTConvert类支持的类型也都可以使用。RCTConvert还提供了一系列辅助函数,用来接收一个JSON值并转换到原生Objective-C类型或类。

CalendarManager中,我们需要把事件的时间交给原生方法,但是我们无法在桥接通道里传递Date类型对象,所以需要把日期转换成字符串或数字来传递。因此我们的原生函数可以这样实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch) {
NSDate *date = [RCTConvert NSDate:sencondsSinceUnixEpoch];
}

// 或者可以这样写

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端,对于函数调用时的参数,也可以有两种形式

1
2
3
4
5
6
7
8
9
10
11
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
date.getTime()
); // 把日期以unix时间戳形式传递

CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
date.toISOString()
); // 把日期以ISO-8601的字符串形式传递

但当错误的类型被传递进入后,将会产生error。

随着参数的越来越多,越来越复杂,出现了一些可选参数的时候。我们可以考虑使用Dictionary类型来存放

1
2
3
4
5
6
7
8
#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中的调用如下:

1
2
3
4
5
CalendarManager.addEvent('Birthday Party', {
location: '4 Privet Drive, Surrey',
time: data.getTime();
description: '...'
});

注意:关于数组和映射

Objective-C并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望获得一个字符串数组,但如果你在JavaScript中调用的时候提供了一个混合Number和String的数组。你会收到一个NSArray,里面既有NSNumber也有NSString。对于数组来说,RCTConvert提供了一些类型化的集合,譬如NSStringArray或者UIColorArray,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用RCTConvert的辅助方法来检测和转换值的类型。

回调函数

原生模块还支持一种特殊的参数——回调函数。它提供一个函数来把返回值传回给JavaScript

1
2
3
4
RCT_EXPORT_METHOD(findeEvents:(RCTResponseSenderBlock)callback) {
NSArray *event = ...;
callback(@[NSNull null], events);
}

RCTResponseSenderBlock类型的函数只接受一个参数——传递给JavaScript回调函数的参数数组。

在上例子中,我们使用了回调函数的常见参数形式,第一个参数为一个错误对象,无错误发生时为null,剩余部分为函数返回值。因此我们可以在JS中以如下方式调用

1
2
3
4
5
6
7
CalendarManager.findEvents((error, events) => {
if(error) {
console.error(error);
} else {
this.setState({ events: events });
}
});

原生模块通常只因调用回调函数一次。但是,它可以保存callback并在将来调用。这在那些通过委托函数来获取返回值的iOS API中最常见。RCTAlertMangaer中就属于这种情况。

如果你想传递一个更接近Error类型的对象给JavaScript,可以使用RCTUtils.h提供的RCTMakeError函数。该函数目前只能发送一个和Error结构以信仰的dictionary给JavaScript。

Promise

原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的async/await语法则效果更佳。如果桥接原生方法的最后两个参数是RCTPromiseResolveBlocakRCTPromiseRejectBlock,则对应的JS方法就回返回一个Promise对象。

我们将上例用Promise来进行重构:

1
2
3
4
5
6
7
8
9
10
11
12
RCT_REMAP_METHOD(findEvents, 
findEventsWithResolver:(RCTPromiseResolveBlock)
rejecter:(RCTPromiseRejectBlock)reject
) {
NSArray *events = ...;
if(evnets){
resolve(events);
} else {
NSError *error = ...;
reject(@"no_events", @"There were no events", error);
}
}

现在JavaScript端的方法回返回一个Promise。这样你就可以在一个声明了async的异步函数内使用await关键来调用,并等待其结果返回。

1
2
3
4
5
6
7
8
9
async function updateEvents() {
try {
const events = await CalendarManager.findEvents();
this.setState({ events });
} catch(e) {
console.error(e);
}
}
updateEvents();

导出常量

原生模块可以导出一些常量,这些常量在JavaScript端随时都可以访问。用这种方法传递一些静态数据,可以避免通过bridge进行一次来回交互。

1
2
3
- (NSDictionary*) constantsToExport {
return@{@"firstDayOfTheWeek": @"Monday"};
}

JavaScript端可以随时同步的访问这个数据:

1
console.log(CalendarManager.firstDayOfTheWeek);

注意:这个常量只在初始化的时候导出一次,所以即使你在运行期间改变constantToExport返回的值,也不会影响到JavaScript环境下所得到的结果。

给JavaScript端发送事件

即使没有被JavaScript调用,原生模块也可以给JavaScript发送事件通知。最好的方法是继承RCTEventEmitter,实现supportEvents方法,并调用self sendEventWithName:

1
2
3
4
5
6
7
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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实例来订阅这些事件

1
2
3
4
5
6
7
8
9
10
11
12
import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;

const calendarManagerEmitter = new NativeEventEmitter(Calendarmanager);

const subscription = calendarManagerEmitter.addListener(
'EventReminder',
(reminder) => console.log(reminder.name);
)

// 取消订阅
subscription.remove()