有时候App需要访问平台API,但React Native可能还没有相应的模块封装;或者你需要复用Objective-C、Swift或C++代码,而不是用JavaScript重新实现一遍;又或者你需要实现某些高性能、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。

我们把React Native设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果React Native还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。

本文是关于如何封装原生模块的高级向导,我们假设您已经具备Objective-C或者Swift,以及iOS核心库(Foundation、UIKit)的相关知识。

iOS 日历模块演示

本向导将会用iOS日历API作为示例。我们的目标就是在Javascript中可以访问到iOS的日历功能。

在React Native中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的Objective-C类,其中RCT是ReaCT的缩写。

// CalendarManager.h
#import "RCTBridgeModule.h"@interface CalendarManager : NSObject <RCTBridgeModule>
@end

为了实现RCTBridgeModule协议,你的类需要包含RCT_EXPORT_MODULE()宏。这个宏也可以添加一个参数用来指定在Javascript中访问这个模块的名字。如果你不指定,默认就会使用这个Objective-C类的名字。

// CalendarManager.m
@implementation CalendarManagerRCT_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里可以这样调用这个方法:

var CalendarManager = require('react-native').NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');

注意: Javascript方法名

导出到Javascript的方法名是Objective-C的方法名的第一个部分。React Native还定义了一个RCT_REMAP_METHOD()宏,它可以指定Javascript方法名。当许多方法的第一部分相同的时候用它来避免在Javascript端的名字冲突。

桥接到Javascript的方法返回值类型必须是void。React Native的桥接操作是异步的,所以要返回结果给Javascript,你必须通过回调或者触发事件来进行。(参见本文档后面的部分)

参数类型

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

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

在我们的CalendarManager例子里,我们需要把事件的时间交给原生方法。我们不能在桥接通道里传递Date对象,所以需要把日期转化成字符串或数字来传递。我们可以这么实现原生函数:

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(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 "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的辅助方法来检测和转换值的类型。

回调函数

警告

本章节内容目前还处在实验阶段,因为我们还并没有太多的实践经验来处理回调函数。

原生模块还支持一种特殊的参数——回调函数。它提供了一个函数来把返回值传回给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对象。

Promises

译注:这一部分涉及到较新的js语法和特性,不熟悉的读者建议先阅读ES6的相关书籍和文档。

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

我们把上面的代码用promise来代替回调进行重构:

RCT_REMAP_METHOD(findEvents,resolver:(RCTPromiseResolveBlock)resolverejecter:(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();

多线程

原生模块不应对自己被调用时所处的线程做任何假设。React Native在一个独立的串行GCD队列中调用原生模块的方法,但这属于实现的细节,并且可能会在将来的版本中改变。通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行。具体来说,如果模块需要调用一些必须在主线程才能使用的API,那应当这样指定:

- (dispatch_queue_t)methodQueue
{return dispatch_get_main_queue();
}

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

- (dispatch_queue_t)methodQueue
{return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

指定的methodQueue会被你模块里的所有方法共享。如果你的方法中“只有一个”是耗时较长的(或者是由于某种原因必须在不同的队列中运行的),你可以在函数体内用dispatch_async方法来在另一个队列执行,而不影响其他方法:

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// 在这里执行长时间的操作...// 你可以在任何线程/队列中执行回调函数callback(@[...]);});
}

注意: 在模块之间共享分发队列

methodQueue方法会在模块被初始化的时候被执行一次,然后会被React Native的桥接机制保存下来,所以你不需要自己保存队列的引用,除非你希望在模块的其它地方使用它。但是,如果你希望在若干个模块中共享同一个队列,则需要自己保存并返回相同的队列实例;仅仅是返回相同名字的队列是不行的。

导出常量

原生模块可以导出一些常量,这些常量在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)animationcompletion:(RCTResponseSenderBlock)callback)

你的枚举现在会用上面提供的选择器进行转换(上面的例子中是integerValue),然后再传递给你导出的函数。

给Javascript发送事件

即使没有被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中可以这样订阅事件:

var { NativeAppEventEmitter } = require('react-native');var subscription = NativeAppEventEmitter.addListener('EventReminder',(reminder) => console.log(reminder.name)
);
...
// 千万不要忘记忘记取消订阅, 通常在componentWillUnmount函数中实现。
subscription.remove();

更多的给JavaScript发送事件的例子,参见RCTLocationObserver.

从Swift导出

Swift不支持宏,所以从Swift向React Native导出类和函数需要多做一些设置,但是大致与Objective-C是相同的。

假设我们已经有了一个一样的CalendarManager,不过是用Swift实现的类:

// 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

请注意,一旦你在IOS中混用2种语言, 你还需要一个额外的桥接头文件,称作“bridging header”,用来导出Objective-C文件给Swift。如果你是通过Xcode菜单中的File>New File来创建的Swift文件,Xcode会自动为你创建这个头文件。在这个头文件中,你需要引入RCTBridgeModule.h

// CalendarManager-Bridging-Header.h
#import "RCTBridgeModule.h"

同样的,你也可以使用RCT_EXTERN_REMAP_MODULERCT_EXTERN_REMAP_METHOD来改变导出模块和方法的JavaScript调用名称。 了解更多信息,请参阅RCTBridgeModule.

本文转自React Native中文网:http://reactnative.cn/docs/0.20/native-modules-ios.html#content

React Native使用指南-原生模块相关推荐

  1. React Native使用指南-原生UI组件

    在如今的App中,已经有成千上万的原生UI部件了--其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多.React Native已经封装了大部分最常见的组件,譬如Scr ...

  2. React Native开发指南-在原生和React Native间通信

    通过植入原生应用和原生UI组件两篇文档,我们学习了React Native和原生组件的互相整合.在整合的过程中,我们会需要在两个世界间互相通信.有些方法已经在其他的指南中提到了,这篇文章总结了所有可行 ...

  3. 【React Native】在原生和React Native间通信(RN调用原生)

    一.从React Native中调用原生方法(原生模块) 原生模块是JS中也可以使用的Objective-C类.一般来说这样的每一个模块的实例都是在每一次通过JS bridge通信时创建的.他们可以导 ...

  4. React Native使用指南-植入原生应用

    由于React并没有假设你其余部分的技术栈--它通常只作为MVC模型中的V存在--它也很容易嵌入到一个并非由React Native开发的应用当中.实际上,它可以和常见的许多工具结合,譬如CocoaP ...

  5. rn+与android+交互,React native 与Android原生交互方式(一)

    前言## 最近在做React Native开发的时候避免不了的需要原生模块和JS之间进行交互,其实RN和原生的通信大致分为两种情况:一种是Android主动向RN端发送事件和数据,另外一种是RN端被动 ...

  6. Android方法调用实体类的值,React Native调用Android原生方法和传值

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 创建react native 项目:react-native init callAndroidProject cd ca ...

  7. 【React Native】iOS原生导航跳转RN页面

    上一篇介绍了React Native使用react-navigation进行导航跳转页面,现在我们介绍下原生iOS中怎么导航进一个新的React Native页面. 一.原生跳转React Nativ ...

  8. React Native使用指南-使用链接库

    并不是所有的APP都需要使用全部的原生功能,包含支持全部特性的代码会增大应用的体积.但我们仍然希望能让你简单地根据自己的需求添加需要的特性. 在这种思想下,我们把许多特性都发布成为互不相关的静态库. ...

  9. React Native调用Android原生代码实现车牌识别功能【附效果图附源码】

    转载请注明出处,原文地址:http://blog.csdn.net/lucherr/article/details/71908180 这段时间研究了下React Native,Facebook推出的, ...

最新文章

  1. python脚本备份网络交换的命令
  2. Python连接MySQL数据库执行sql语句时的参数问题
  3. OneNote代码高亮插件(NoteHighLight)
  4. 请简单解释一下ARP协议和ARP攻击
  5. ashx PHP文件 优劣,.NET_后缀为 ashx 与 axd 的文件区别浅析,唯一不同的地方是:axd扩展名 - phpStudy...
  6. 闪屏页面(Splash)开发
  7. Mac下配置sublime实现LaTeX
  8. java xms xmx 默认值_JVM启动参数-Xmx的默认值是多少?
  9. 软件测试—软件测试基础知识—测试用例设计的方法判定表和因果图
  10. html5首页图标怎么除掉,移动端H5页面端如何除去input输入框的默认样式
  11. 100阶乘c语言如何实现,求10000的阶乘(c语言代码实现)
  12. tp5 获取当前的url方法
  13. c++中显示“无法打开xxx.exe进行写入”
  14. O2O常见的结算模式是什么 O2O线上线下营销策略有哪些?
  15. python弧度制转换 三角函数 反三角函数 双曲 反双曲 sin cos tan asin acos atan asinh acosh atanh atanh2
  16. rsyncd.conf 文件man手册翻译
  17. mysql中高阶玩法系列(八)
  18. SpringMVC cookie设置Secure
  19. flume采集hive日志写到hdfs问题
  20. dede网站模板制作流程

热门文章

  1. html5 填表 表单 input output 与表单验证
  2. SQL Server数据库大型应用解决方案总结【转】
  3. Beta版本测试报告
  4. mybatis大于小于的转义
  5. C# Obsolete
  6. 区分Activity的四种加载模式-activity
  7. Android应用程序变量
  8. 我想知道怎么求N的N次方
  9. Server.Transfer 和Response.Redirect 比较
  10. 设计模式——Decorator 装饰模式