(一)前言
今天我们继续来看一下原生模块的一些特性例如:回调方法函数,Promises,多线程,常量设置,事件发送到JavaScript,监听生命周期事件,获取封装Swift原生模块等相关的特性。
(二)Callback
原生模块同时支持一个特殊的参数类型-回调函数。在很多实例中,可以提供一个方法进行调用把数据传递给JavaScript。使用方法如下:
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]);
}
RCTResponseSenderBlock只能接受一个参数,为传递给JavaScript回调方法的参数数组。在当前的例子中我们使用Node.js的一些开发习惯:第一个参数为error对象(当然没有错误信息的时候,默认为null),另外的参数为该回调方法的返回值信息,看一下JavaScript调用方法:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
import React,{
AppRegistry,
Component,
StyleSheet,
Text,
View,
TouchableHighlight,
} from 'react-native';
///进行导入NativeModules中的CalendarManger模块
import { NativeModules } from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
class CustomButton extends React.Component {
render() {
return (
<TouchableHighlight
style={styles.button}
underlayColor="#a5a5a5"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>{this.props.text}</Text>
</TouchableHighlight>
);
}
}
class ModulesDemo extends Component {
constructor(props){
super(props);
this.state={
events:'',
}
}
render() {
return (
<View style={{marginTop:20}}>
<Text style={styles.welcome}>
封装iOS原生模块实例
</Text>
'Callback的返回数据为:'+{this.state.events}
</Text>
<CustomButton text="点击调用原生模块findEvents方法-Callback"
onPress={()=>CalendarManager.findEvents((error,events)=>{
if(error){
console.error(error);
}else{
this.setState({events:events,});
}
}
)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
button: {
margin:5,
backgroundColor: 'white',
padding: 10,
borderWidth:1,
borderColor: '#cdcdcd',
},
});
AppRegistry.registerComponent('ModulesDemo', () => ModulesDemo);
原生模块调用回调方法只会支持调用一次,但是你可以保存该callback然后在以后某个时间使用。
(三)Promises
看了上面的回调函数的使用,大家有没有发现上面的写法还有有一些繁琐的?OK 当然原生模块还可以支持使用Promise,这样可以简化我们编写的代码。如果大家搭配使用ES2016标准的async/await的语法使用会更加好。如果被桥接的原生方法的最后一个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock类型,那么该JS方法会返回一个Promise对象。下面我们使用Promise对象来进行重构之前的回调函数方法。具体代码如下:
RCT_REMAP_METHOD(findEventsPromise,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events =@[@"张三",@"李四",@"王五",@"赵六"];
if (events) {
resolve(events);
} else {
NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
reject(@"no_events", @"There were no events", error);
}
}
经过这样处理之后,JavaScript端的方法会返回一个Promise对象,这样你可以在async关键字修饰的方法中使用await关键字进行处理来等待数据返回,具体JavaScript端中调用处理方法如下:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
import React,{
AppRegistry,
Component,
StyleSheet,
Text,
View,
TouchableHighlight,
} from 'react-native';
///进行导入NativeModules中的CalendarManger模块
import { NativeModules } from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
class CustomButton extends React.Component {
render() {
return (
<TouchableHighlight
style={styles.button}
underlayColor="#a5a5a5"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>{this.props.text}</Text>
</TouchableHighlight>
);
}
}
class ModulesDemo extends Component {
constructor(props){
super(props);
this.state={
events:'',
}
}
//获取Promise对象处理
async _updateEvents(){
try{
var events=await CalendarManager.findEventsPromise();
this.setState({events});
}catch(e){
console.error(e);
}
}
render() {
return (
<View style={{marginTop:20}}>
<Text style={styles.welcome}>
封装iOS原生模块实例
</Text>
<Text style={{marginLeft:5}}>
'Callback的返回数据为:'+{this.state.events}
</Text>
<CustomButton text="点击调用原生模块findEventsPromise方法-Callback"
onPress={()=>CalendarManager.findEvents((error,events)=>{
if(error){
console.error(error);
}else{
this.setState({events:events,});
}
}
)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
button: {
margin:5,
backgroundColor: 'white',
padding: 10,
borderWidth:1,
borderColor: '#cdcdcd',
},
});
AppRegistry.registerComponent('ModulesDemo', () => ModulesDemo);
(四)Threading(多线程)
原生模块被调用运行的线程,我们一般不应该去进行修改配置。React Native会在一个独立的串行GCD队列中调用原生模块,不过将来该方式可能会发生变化。通过- (dispatch_queue_t)methodQueue方法我们可以指定具体运行的线程。例如:如果我们需要调用一些必须要在主线程运行的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方法来在另一个线程中运行,而不去影响其他方法,具体使用方法:
//对外提供调用方法,演示Thread使用
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在后台执行耗时操作
// You can invoke callback from any thread/queue
callback(@[[NSNull null],@"耗时操作执行完成..."]);
});
}
(五)Exporting Constants 封装常量供调用
原生封装模块可以封装提供常量数据给JavaScript在随时调用,这样可以通过桥接通信来传递一些静态数据。使用方式:
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
然后JavaScript可以同步进行访问这个数据
console.log(CalendarManager.firstDayOfTheWeek);
但是对于静态数据,我们应该知道该方法封装的数据只会初始化返回一次,也就是说如果在运行过程中你修改了constantsToExport的返回值,也不会影响到JavaScript端调用的结果。
(六)Enum Constants 封装枚举常量供调用
使用NS_ENUM定义的枚举的类型需要扩展RCTConvert方法之后,然后作为方法中传递的参数。例如我们需要进行封装如下的枚举定义
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
接下来,你可以定义封装方法,然后封装枚举常量给JavaScript进行使用
- (NSDictionary *)constantsToExport
{
return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) }
};
RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
completion:(RCTResponseSenderBlock)callback)
现在你创建的枚举会用上面的方法中的类型进行转换(例子中为integerValue),然后会传递给你封装的方法。
(七)发送事件给JavaScript
原生模块可以在没有被调用的情况下直接发送事件给JavaScript端,最简单的方式就是使用eventDispatcher。原生模块的处理方式如下(全部代码大家到时候见项目实例):
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation CalendarManager
@synthesize bridge = _bridge;
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *eventName = notification.userInfo[@"name"];
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
body:@{@"name": eventName}];
}
@end
JavaScript然后按照如下的方式进行订阅接收事件
import { NativeAppEventEmitter } from 'react-native';
var subscription = NativeAppEventEmitter.addListener(
'EventReminder',
(reminder) => console.log(reminder.name)
);
...
// Don't forget to unsubscribe, typically in componentWillUnmount
subscription.remove();
有关更多的给JavaScript发送事件的例子,可以参考RCTLocationObserver
下面来看一下我这边写的实例,里边的代码可能包括以上特性测试代码,
首先看一下Objective-C代码:
//
// CalendarManager.m
// ModulesDemo
//
// Copyright © 2016年 Facebook. All rights reserved.
//
#import "CalendarManager.h"
#import "RCTConvert.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation CalendarManager
@synthesize bridge=_bridge;
//默认名称
RCT_EXPORT_MODULE()
//对外提供调用方法
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location){
NSLog(@"Pretending to create an event %@ at %@", name, location);
}
//对外提供调用方法,为了演示事件时间格式化 secondsSinceUnixEpoch
RCT_EXPORT_METHOD(addEventMore:(NSString *)name location:(NSString *)location data:(NSNumber*)secondsSinceUnixEpoch){
NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}
//对外提供调用方法,为了演示事件时间格式化 ISO8601DateString
RCT_EXPORT_METHOD(addEventMoreTwo:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}
//对外提供调用方法,为了演示事件时间格式化 自动类型转换
RCT_EXPORT_METHOD(addEventMoreDate:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init] ;
[formatter setDateFormat:@"yyyy-MM-dd"];
NSLog(@"获取的事件信息:%@,地点:%@,时间:%@",name,location,[formatter stringFromDate:date]);
}
//对外提供调用方法,为了演示事件时间格式化 传入属性字段
RCT_EXPORT_METHOD(addEventMoreDetails:(NSString *)name details:(NSDictionary *) dictionary)
{
NSString *location = [RCTConvert NSString:dictionary[@"location"]];
NSDate *time = [RCTConvert NSDate:dictionary[@"time"]];
NSString *description=[RCTConvert NSString:dictionary[@"description"]];
NSLog(@"获取的事件信息:%@,地点:%@,时间:%@,备注信息:%@",name,location,time,description);
}
//对外提供调用方法,演示Callback
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events=@[@"张三",@"李四",@"王五"];
callback(@[[NSNull null],events]);
}
//对外提供调用方法,演示Promise使用
RCT_REMAP_METHOD(findEventsPromise,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events =@[@"张三",@"李四",@"王五",@"赵六"];
if (events) {
resolve(events);
} else {
NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
reject(@"no_events", @"There were no events", error);
}
}
//对外提供调用方法,演示Thread使用
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在后台执行耗时操作
// You can invoke callback from any thread/queue
callback(@[[NSNull null],@"耗时操作执行完成..."]);
});
}
//进行设置封装常量给JavaScript进行调用
-(NSDictionary *)constantsToExport{
return @{@"firstDayOfTheWeek":@"Monday"};
}
//进行触发发送通知事件
RCT_EXPORT_METHOD(sendNotification:(NSString *)name){
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(calendarEventReminderReceived:) name:nil object:nil];
}
//进行设置发送事件通知给JavaScript端
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
body:@{@"name": @"张三"}];
}
@end
下面为JavaScript前端的代码
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
import React,{
AppRegistry,
Component,
StyleSheet,
Text,
View,
TouchableHighlight,
} from 'react-native';
///进行导入NativeModules中的CalendarManger模块
import { NativeModules } from 'react-native';
import { NativeAppEventEmitter } from 'react-native';
var subscription;
var CalendarManager = NativeModules.CalendarManager;
class CustomButton extends React.Component {
render() {
return (
<TouchableHighlight
style={styles.button}
underlayColor="#a5a5a5"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>{this.props.text}</Text>
</TouchableHighlight>
);
}
}
class ModulesDemo extends Component {
constructor(props){
super(props);
this.state={
events:'',
notice:'',
}
}
componentDidMount(){
console.log('开始订阅通知...');
subscription = NativeAppEventEmitter.addListener(
'EventReminder',
(reminder) => console.log('通知信息:'+reminder.name)
);
}
componentWillUnmount(){
subscription.remove();
}
//获取Promise对象处理
async _updateEvents(){
try{
var events=await CalendarManager.findEventsPromise();
this.setState({events});
}catch(e){
console.error(e);
}
}
render() {
return (
<View style={{marginTop:20}}>
<Text style={styles.welcome}>
封装iOS原生模块实例
</Text>
<CustomButton text="点击调用原生模块addEvent方法"
onPress={()=>CalendarManager.addEvent('生日聚会', '江苏南通 中天路')}
/>
<CustomButton text="点击调用原生模块addEventMoreDate方法"
onPress={()=>CalendarManager.addEventMoreDate('生日聚会', '江苏南通 中天路',1463987752)}
/>
<CustomButton text="调用原生模块addEventMoreDetails方法-传入字段格式"
onPress={()=>CalendarManager.addEventMoreDetails('生日聚会', {
location:'江苏 南通市 中天路',
time:1463987752,
description:'请一定准时来哦~'
})}
/>
<Text style={{marginLeft:5}}>
'Callback的返回数据为:'+{this.state.events}
</Text>
<CustomButton text="点击调用原生模块findEvents方法-Callback"
onPress={()=>CalendarManager.findEvents((error,events)=>{
if(error){
console.error(error);
}else{
this.setState({events:events,});
}
}
)}
/>
<CustomButton text="点击调用原生模块findEventsPromise方法-Promise"
onPress={()=>CalendarManager.findEvents((error,events)=>{
if(error){
console.error(error);
}else{
this.setState({events:events,});
}
}
)}
/>
<Text style={{marginLeft:5}}>
'静态数据为:'+{CalendarManager.firstDayOfTheWeek}
</Text>
<Text style={{marginLeft:5}}>
'发送通知信息:'+{this.state.notice}
</Text>
<CustomButton text="点击调用原生模块sendNotification方法"
onPress={()=>CalendarManager.sendNotification('准备发送通知...')}
/>
</View>
);
}
}
const styles = StyleSheet.create({
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
button: {
margin:5,
backgroundColor: 'white',
padding: 10,
borderWidth:1,
borderColor: '#cdcdcd',
},
});
AppRegistry.registerComponent('ModulesDemo', () => ModulesDemo);
** (八)封装Swift方法**
Swift是不支持宏定义的,所有如果需要封装Swift中一些模块和方法给JavaScript进行调用就需要更多的配置,不过基本和Obejctitve-C中封装配置方法差不多的。
现在例子你有一个Swfit类CalendarManager
// CalendarManager.swift
@objc(CalendarManager)
class CalendarManager: NSObject {
@objc func addEvent(name: String, location: String, date: NSNumber) -> Void {
// Date is ready to use!
}
}
[特别注意].这边你需要使用@objc标签进行修饰封装的类和方法,来确保可以让Objective-C可以访问
然后我们创建一个私有的实现类,在React Native桥接中注册相关必要的信息。具体代码如下:
/ CalendarManagerBridge.m
#import "RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)
RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)
@end
当你在你的项目中同时使用两个语言进行开发你需要创建额外的桥接文件,该文件称为桥接头文件。该用来提供Objective-C文件给Swift进行调用。如果你通过Xcode IDE 选择文件夹-创建新文件添加Swift文件到你的项目中,那么Xcode会自动给你创建头文件,你只需要在头文件中导入RCTBridgeModule.h 。具体代码如下:
// CalendarManager-Bridging-Header.h
#import "RCTBridgeModule.h"
同样的,你也可以使用RCT_EXTERN_REMAP_MODULE和RCT_EXTERN_REMAP_METHOD来设置模块和方法的JavaScript调用名称。
打个小广告:这是我正在撸的RN项目,目前完成的有网易新闻、美团>>>,还会继续添加一些功能,希望支持~~~