JavaScriptCore框架 是一个苹果在iOS7引入的框架,该框架让 Objective-C 和 JavaScript 代码直接的交互变得更加的简单方便。
而JavaScriptCore是苹果Safari浏览器的JavaScript引擎,或许你听过Google的V8引擎,在WWDC上苹果演示了最新的Safari,据说JavaScript处理速度已经大大超越了Google的Chrome,这就意味着JavaScriptCore在性能上也不输V8了。
1.原生UI组件的编写
1.编写原生组件继承 RCTViewManager
接下来你需要一些Javascript代码来让这个视图变成一个可用的React组件:
提供原生视图很简单:
- 首先创建一个子类
- 添加
标记宏RCT_EXPORT_MODULE()
- 实现
方法-(UIView *)view
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
接下来你需要一些Javascript代码来让这个视图变成一个可用的React组件:
// MapView.js
var { requireNativeComponent } = require('react-native');
// requireNativeComponent 自动把这个组件提供给 "RNTMapManager"
module.exports = requireNativeComponent('RNTMap', null);
现在我们就已经实现了一个完整功能的地图组件了,诸如捏放和其它的手势都已经完整支持。但是现在我们还不能真正的从Javascript端控制它。(╯﹏╰)
属性
我们能让这个组件变得更强大的第一件事情就是要能够封装一些原生属性供Javascript使用。举例来说,我们希望能够禁用手指捏放操作,然后指定一个初始的地图可见区域。禁用捏放操作只需要一个布尔值类型的属性就行了,所以我们添加这么一行:
// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
注意我们现在把类型声明为
BOOL
类型——React Native用
RCTConvert
来在JavaScript和原生代码之间完成类型转换。如果转换无法完成,会产生一个“红屏”的报错提示,这样你就能立即知道代码中出现了问题。如果一切进展顺利,上面这个宏就已经包含了导出属性的全部实现。
现在要想禁用捏放操作,我们只需要在JS里设置对应的属性:
// MyApp.js
<MapView pitchEnabled={false} />
但这样并不能很好的说明这个组件的用法——用户要想知道我们的组件有哪些属性可以用,以及可以取什么样的值,他不得不一路翻到Objective-C的代码。要解决这个问题,我们可以创建一个封装组件,并且通过
PropTypes
来说明这个组件的接口。
// MapView.js
import React, { Component, PropTypes } from 'react';
import { requireNativeComponent } from 'react-native';
var RNTMap = requireNativeComponent('RNTMap', MapView);
import PropTypes from 'prop-types';
export default class MapView extends Component {
static propTypes = {
/**
* 当这个属性被设置为true,并且地图上绑定了一个有效的可视区域的情况下,
* 可以通过捏放操作来改变摄像头的偏转角度。
* 当这个属性被设置成false时,摄像头的角度会被忽略,地图会一直显示为俯视状态。
*/
pitchEnabled: PropTypes.bool,
};
render() {
return <RNTMap {...this.props} />;
}
}
译注:使用了封装组件之后,你还需要注意到module.exports导出的不再是requireNativeComponent的返回值,而是所创建的包装组件。
现在我们有了一个封装好的组件,还有了一些注释文档,用户使用起来也更方便了。注意我们现在把
requireNativeComponent
的第二个参数从null变成了用于封装的组件
MapView
。这使得React Native的底层框架可以检查原生属性和包装类的属性是否一致,来减少出现问题的可能。
现在,让我们添加一个更复杂些的
region
属性。我们首先添加原生代码:
// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RNTMap)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
这里还可以通过bridge提供的方法直接调用JSModule
/**
* This method is used to call functions in the JavaScript application context.
* It is primarily intended for use by modules that require two-way communication
* with the JavaScript code. Safe to call from any thread.
*/
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args;
- (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion;
2.原生功能模块的编写
在React Native中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的Objective-C类,其中RCT是ReaCT的缩写。
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
为了实现
RCTBridgeModule
协议,你的类需要包含
RCT_EXPORT_MODULE()
宏。这个宏也可以添加一个参数用来指定在Javascript中访问这个模块的名字。如果你不指定,默认就会使用这个Objective-C类的名字。
// CalendarManager.m
@implementation CalendarManager
RCT_EXPORT_MODULE();
@end
你必须明确的声明要给Javascript导出的方法,否则React Native不会导出任何方法。声明通过
RCT_EXPORT_METHOD()
宏来实现:
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的方法名是Objective-C的方法名的第一个部分。React Native还定义了一个
RCT_REMAP_METHOD()
宏,它可以指定Javascript方法名。当许多方法的第一部分相同的时候用它来避免在Javascript端的名字冲突。
桥接到Javascript的方法返回值类型必须是
。React Native的桥接操作是异步的,所以要返回结果给Javascript,你必须通过回调或者触发事件来进行。(参见本文档后面的部分)
void
参数类型
RCT_EXPORT_METHOD
支持所有标准JSON类型,包括:
- string (
)NSString
- number (
,NSInteger
,float
,double
,CGFloat
)NSNumber
- boolean (
,BOOL
)NSNumber
- array (
) 包含本列表中任意类型NSArray
- object (
) 包含string类型的键和本列表中任意类型的值NSDictionary
- 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.toTime(),
description: '...'
})
注意: 关于数组和映射
Objective-C并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望收到一个字符串数组,但如果JavaScript在调用的时候提供了一个混合number和string的数组,你会收到一个
,里面既有
NSArray
也有
NSNumber
。对于数组来说,
NSString
提供了一些类型化的集合,譬如
RCTConvert
或者
NSStringArray
,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用
UIColorArray
的辅助方法来检测和转换值的类型。
RCTConvert
回调函数
从源码我们可以看出,RN提供了两种回调方式:普通回调,Promise回调。
/**
* The type of a block that is capable of sending a response to a bridged
* operation. Use this for returning callback methods to JS.
*/
typedef void (^RCTResponseSenderBlock)(NSArray *response);
/**
* The type of a block that is capable of sending an error response to a
* bridged operation. Use this for returning error information to JS.
*/
typedef void (^RCTResponseErrorBlock)(NSError *error);
/**
* Block that bridge modules use to resolve the JS promise waiting for a result.
* Nil results are supported and are converted to JS's undefined value.
*/
typedef void (^RCTPromiseResolveBlock)(id result);
/**
* Block that bridge modules use to reject the JS promise waiting for a result.
* The error may be nil but it is preferable to pass an NSError object for more
* precise error messages.
*/
typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);
原生模块还支持一种特殊的参数——回调函数。它提供了一个函数来把返回值传回给JavaScript。
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]);
}
RCTResponseSenderBlock
只接受一个参数——传递给JavaScript回调函数的参数数组。在上面这个例子里我们用Node.js的常用习惯:第一个参数是一个错误对象(没有发生错误的时候为null),而剩下的部分是函数的返回值。
CalendarManager.findEvents((error, events) => {
if (error) {
console.error(error);
} else {
this.setState({events: events});
}
})
原生模块通常只应调用回调函数一次。但是,它可以保存callback并在将来调用。这在封装那些通过“委托函数”来获得返回值的iOS API时最为常见。
RCTAlertManager
中就属于这种情况。
如果你想传递一个更接近
Error
类型的对象给Javascript,可以用
RCTUtils.h
提供的
RCTMakeError
函数。现在它仅仅是发送了一个和Error结构一样的dictionary给Javascript,但我们考虑在将来版本里让它产生一个真正的
Error
对象。
注意
如果你传递了回调函数,那么在原生端就必须执行它(如果传递了两个,比如onSuccess和onFail,那么执行其中一个即可),否则会导致内存泄漏。
Promises
译注:这一部分涉及到较新的js语法和特性,不熟悉的读者建议先阅读ES6的相关书籍和文档。
原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的
async/await
语法则效果更佳。如果桥接原生方法的最后两个参数是
RCTPromiseResolveBlock
和
RCTPromiseRejectBlock
,则对应的JS方法就会返回一个Promise对象。
我们把上面的代码用promise来代替回调进行重构:
RCT_REMAP_METHOD(findEvents,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events = ...
if (events) {
resolve(events);
} else {
reject(error);
}
}
现在JavaScript端的方法会返回一个Promise。这样你就可以在一个声明了
async
的异步函数内使用
await
关键字来调用,并等待其结果返回。(虽然这样写着看起来像同步操作,但实际仍然是异步的,并不会阻塞执行来等待)。
async function updateEvents() {
try {
var events = await CalendarManager.findEvents();
this.setState({ events });
} catch (e) {
console.error(e);
}
}
updateEvents();
导出常量
原生模块可以导出一些常量,这些常量在JavaScript端随时都可以访问。用这种方法来传递一些静态数据,可以避免通过bridge进行一次来回交互。
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
Javascript端可以随时同步地访问这个数据:
console.log(CalendarManager.firstDayOfTheWeek);
但是注意这个常量仅仅在初始化的时候导出了一次,所以即使你在运行期间改变
constantToExport
返回的值,也不会影响到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
接着你可以这样定义方法并且导出enum值作为常量:
- (NSDictionary *)constantsToExport
{
return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) }
};
RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
completion:(RCTResponseSenderBlock)callback)
你的枚举现在会用上面提供的选择器进行转换(上面的例子中是
integerValue
),然后再传递给你导出的函数。
详细介绍请参考 RN 原生模块