首页 » React Native » React Native技术文章 » 正文

【React Native开发】React Native进阶之原生模块封装特性篇详解-适配iOS开发(60)

尊重版权,未经授权不得转载

本文来自:江清清的技术专栏(http://www.lcode.org)

(一)前言

今天我们继续来看一下原生模块的一些特性例如:回调方法函数,Promises,多线程,常量设置,事件发送到JavaScript,监听生命周期事件,获取封装Swift原生模块等相关的特性。当前所讲解内容适配iOS开发。

刚创建的React Native技术交流5群(386216878),欢迎各位大牛,React Native技术爱好者加入交流!同时博客右侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送!

本文章实例项目地址https://github.com/jiangqqlmj/ModulesDemo

上一篇我们已经讲解过封装原生模块-适配iOS平台,地址:http://www.lcode.org/?p=1691  如果这一篇没看,那么你一定要去先学习一下,因为本讲的知识点需要上一篇做铺垫的哦~

(二)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);

Ok,我们来看一下调用运行的效果,具体的源代码大家可以去点击顶部的项目地址去查看哈.

原生模块调用回调方法只会支持调用一次,但是你可以保存该callback然后在以后某个时间使用。这样的场景通常在封装那些需要代理的iOS APIs比较常见,具体事例大家可以看一下RCTAlertManager的封装。

除此之外,如果你需要封装传递一个更加类似error的对象给JavaScript,那我可以使用RCTUtil.h中的RCTMakeError对象。现在我们暂时是发送一个Error结构一样的字典给JavaScript,但是官方解释讲:在将来可能直接转变成JavaScript真正的Error对象。

(三)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],@"耗时操作执行完成..."]);
  });
}

[特别注意].封装的模块之间共享分发子线程队列

methodQueue方法会在模块初始化的时候被调用,然后会被桥接机制进行保存起来,所以你没必要手动保存该队列,除非你需要在模块的其他地方使用它。但是如果你需要在多个模块中使用同一个队列,那么此时你就需要自己保存并返回该实例;如果仅仅是返回相同的队列名字是没用的啦。

(五)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
//
//  Created by 江清清 on 16/5/22.
//  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调用名称。如果想了解更多可以查看RCTBridgeModule.

(九)运行效果

所有例子整体运行效果如下:

(十)最后总结

今天我们主要学习了一下原生模块的一些特性例如:回调方法函数,Promises,多线程,常量设置,事件发送到JavaScript,监听生命周期事件,获取封装Swift原生模块等相关的特性。当前所讲解内容适配iOS开发。大家有问题可以加一下群React Native技术交流5群(386216878).或者底下进行回复一下。

本文章实例项目地址https://github.com/jiangqqlmj/ModulesDemo

尊重原创,未经授权不得转载:From Sky丶清(http://www.lcode.org/) 侵权必究!

关注我的订阅号(codedev123),每天分享移动开发技术(Android/IOS),项目管理以及博客文章!(欢迎关注,第一时间推送精彩文章)

关注我的微博,可以获得更多精彩内容