React Native 与原生模块数据通信(二)(iOS)

(一)前言

今天我们继续来看一下原生模块的一些特性例如:回调方法函数,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项目,目前完成的有网易新闻、美团>>>,还会继续添加一些功能,希望支持~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容