ReactNative之原生模块开发并发布--iOS篇

移动开发
前段时间做了个ReactNative的App,发现ReactNative中不少组件并不存在,所以还是需要自己对原生模块进行编写让JS调用, 正是因为在这个编写过程中遇到不少问题,发觉了官网文档中许多的不足。所以产生了写一个实践教程的想法,最终有了这么一篇文章。

[[166181]]

前段时间做了个ReactNative的App,发现ReactNative中不少组件并不存在,所以还是需要自己对原生模块进行编写让JS调用, 正是因为在这个编写过程中遇到不少问题,发觉了官网文档中许多的不足。所以产生了写一个实践教程的想法,最终有了这么一篇文章。

整篇文章主要以编写一个原生模块为例子,来讲述了我们在编写原生模块所用到的一些知识,并且在整个例子中,配有了完整的实践代码,方便大家理解并调 试。除了这些内容,文章还讲述了我们如何将自己编写的原生模块发布到npm上分享给别人使用。希望能够给大家带来帮助,也希望大家将自己编写的原生模块分 享出来。

示例代码github地址:https://github.com/liuchungui/react-native-BGNativeModuleExample

准备工作:

创建ReactNative工程

我们需要先创建一个ReactNative工程,使用如下命令创建。

  1. react native init TestProject 

创建好工程之后,我们使用xcode打开TestProject/ios/下的iOS工程。

创建静态库,并将这个静态库手动链接到工程中

首先,我们在前面创建的ReactNative工程下的node_modules创建一个文件夹react-native-BGNativeModuleExample,然后我们在新创建的文件夹下再创建一个ios文件夹。

  1. $ cd TestProject/node_modules 
  2. $ mkdir react-native-BGNativeModuleExample 
  3. $ cd react-native-BGNativeModuleExample 
  4. $ mkdir ios 

然后,由于ReactNative的组件都是一个个静态库,我们发布到npm给别人使用的话,也需要建立静态库。我们使用Xcode建立静态库,取 名为BGNativeModuleExample。建立之后,我们将创建的静态库中的文件全部copy到node_modules/react- native-BGNativeModuleExample/ios目录下。

iOS文件目录如下:

  1. |____BGNativeModuleExample 
  2. | |____BGNativeModuleExample.h 
  3. | |____BGNativeModuleExample.m 
  4. |____BGNativeModuleExample.xcodeproj 

最后,我们需要手动将这个静态库链接到工程中。

1、使用xcode打开创建的静态库,添加一行Header Search Paths,值为$(SRCROOT)/../../react-native/React,并设置为recursive。

2、将BGNativeModuleExample静态库工程拖动到工程中的Library中。 

3、选中 TARGETS => TestProject => Build Settings => Link Binary With Libraries,添加libBGNativeModuleExample.a这个静态库 

到此,我们准备工作完成了。我们这里这么准备是有用意的,那就是模拟npm链接的过程,建立好了环境,避免了发布到npm上后别人使用找不到静态库的问题。

一、编写原生模块代码

1、创建原生模块

选中我们创建的BGNativeModuleExample静态库,然后在BGNativeModuleExample.h文件中导入RCTBridgeModule.h,让BGNativeModuleExample类遵循RCTBridgeModule协议。

  1. //BGNativeModuleExample.h文件的内容如下 
  2. #import #import "RCTBridgeModule.h" 
  3. @interface BGNativeModuleExample : NSObject @end 

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

在这里,我们指定了模块的名字为BGNativeModuleExample。

  1. RCT_EXPORT_MODULE(BGNativeModuleExample); 

实现了RCTBridgeModule协议之后,我们就可以在js中如下获取到我们创建的原生模块。

  1. import { NativeModules } from 'react-native'
  2. var BGNativeModuleExample = NativeModules.BGNativeModuleExample; 

需要注意的是,RCT_EXPORT_MODULE宏传递的参数不能是OC中的字符串。如果传递 @“BGNativeModuleExample",那么我们导出给JS的模块名字其实是@"BGNativeModuleExample",使用 BGNativeModuleExample就找不到了。在这里,我们其实可以通过打印NativeModules来查找到我们创建的原生模块。

2、为原生模块添加方法

我们需要明确的声明要给JS导出的方法,否则ReactNative不会导出任何方法。声明通过RCT_EXPORT_METHOD()宏来实现:

  1. RCT_EXPORT_METHOD(testPrint:(NSString *)name info:(NSDictionary *)info) { 
  2.   RCTLogInfo(@"%@: %@", name, info); 

在JS中,我们可以这样调用这个方法:

  1. BGNativeModuleExample.testPrint("Jack", { 
  2.   height: '1.78m'
  3.   weight: '7kg' 
  4. }); 

3、参数类型

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

  • string (NSString)

  • number (NSInteger, float, double, CGFloat, NSNumber)

  • boolean (BOOL, NSNumber)

  • array (NSArray) 包含本列表中任意类型

  • map (NSDictionary) 包含string类型的键和本列表中任意类型的值

  • function (RCTResponseSenderBlock)

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

了解更多请点击原生模块

4、回调函数

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

回调函数,在官方的文档中是有上面的一个警告,不过在使用过程暂时未发现问题。在OC中,我们添加一个getNativeClass方法,将当前模块的类名回调给JS。

  1. RCT_EXPORT_METHOD(getNativeClass:(RCTResponseSenderBlock)callback) { 
  2.   callback(@[NSStringFromClass([self class])]); 

在JS中,我们通过以下方式获取到原生模块的类名

  1. BGNativeModuleExample.getNativeClass(name => { 
  2.   console.log("nativeClass: ", name); 
  3. }); 

原生模块通常只应调用回调函数一次。但是,它们可以保存callback并在将来调用。这在封装那些通过“委托函数”来获得返回值的iOS API时最常见。

5、Promises

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

我们通过Promises来实现原生模块是否会响应方法,响应则返回YES,不响应则返回一个错误信息,代码如下:

  1. RCT_REMAP_METHOD(testRespondMethod, 
  2.                  name:(NSString *)name 
  3.                  resolver:(RCTPromiseResolveBlock)resolve 
  4.                  rejecter:(RCTPromiseRejectBlock)reject) { 
  5.   if([self respondsToSelector:NSSelectorFromString(name)]) { 
  6.     resolve(@YES); 
  7.   } 
  8.   else { 
  9.     reject(@"-1001", @"not respond this method", nil); 
  10.   } 

在JS中,我们有两种方式调用,第一种是通过then....catch的方式:

  1. BGNativeModuleExample.testRespondMethod("dealloc"
  2.     .then(result => { 
  3.       console.log("result is ", result); 
  4.     }) 
  5.     .catch(error => { 
  6.       console.log(error); 
  7.     }); 

第二种是通过try...catch来调用,与第一种相比,第二种会报警告”Possible Unhandled Promiss Rejection (id:0)“。

  1. async testRespond() { 
  2. try { 
  3.   var result = BGNativeModuleExample.testRespondMethod("hell"); 
  4.   if(result) { 
  5.     console.log("respond this method"); 
  6.   } 
  7. catch (e) { 
  8.   console.log(e); 
  9.   } 

注意: 如果使用Promiss我们不需要参数,则在OC去掉name那一行就行了;如果需要多个参数,在name下面多加一行就行了,注意它们之间不需要添加逗号。

6、多线程

我们这里操作的模块没有涉及到UI,所以专门建立一个串行的队列给它使用,如下:

  1. return dispatch_queue_create("com.liuchungui.demo", DISPATCH_QUEUE_SERIAL); 

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

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

更多线程的操作细节可以参考:http://reactnative.cn/docs/0.24/native-modules-ios.html#content

7、导出常量

原生模块可以导出一些常量,这些常量在JavaScript端随时都可以访问。用这种方法来传递一些静态数据,可以避免通过bridge进行一次来回交互。

OC中,我们实现constantsToExport方法,如下:

  1. - (NSDictionary *)constantsToExport { 
  2.   return @{ @"BGModuleName" : @"BGNativeModuleExample"
  3.             TestEventName: TestEventName 
  4.             }; 

JS中,我们打印一下这个常量

  1. console.log("BGModuleName value is ", BGNativeModuleExample.BGModuleName); 

但是注意这个常量仅仅在初始化的时候导出了一次,所以即使你在运行期间改变constantToExport返回的值,也不会影响到JavaScript环境下所得到的结果。

8、给JS发送事件

即使没有被JS调用,本地模块也可以给JS发送事件通知。最直接的方式是使用eventDispatcher。

在这里,我们为了能够接收到事件,我们开一个定时器,每一秒发送一次事件。

  1. #import "BGNativeModuleExample.h" 
  2. #import "RCTEventDispatcher.h" 
  3. @implementation BGNativeModuleExample 
  4. @synthesize bridge = _bridge; 
  5. - (instancetype)init { 
  6.   if(self = [super init]) { 
  7.     [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendEventToJS) userInfo:nil repeats:YES]; 
  8.   } 
  9.   return self; 
  10. - (void)receiveNotification:(NSNotification *)notification { 
  11.   [self.bridge.eventDispatcher sendAppEventWithName:TestEventName body:@{@"name": @"Jack"}]; 
  12. @end 

在JS中,我们这样接收事件

  1. NativeAppEventEmitter.addListener(BGNativeModuleExample.TestEventName, info => { 
  2.       console.log(info); 
  3.     }); 

注意: 编写OC代码时,需要添加@synthesize bridge = _bridge;,否则接收事件的时候就会报Exception -[BGNativeModuleExample brige]; unrecognized selector sent to instance的错误。

上面原生代码就编写好了,主要以代码实践为主,弥补官方文档中的一些不足,如果要需要了解更多的原生模块封装的知识,可以参考原生模块,也可以参考官方的源代码。

二、发布上线

我们按照上面步骤编写好原生模块之后,接下来将我们写的原生模块发布到npm。

1、我们需要创建github仓库

在github上创建一个仓库react-native-BGNativeModuleExample,然后关联到我们前面创建的react-native-BGNativeModuleExample目录

  1. $ cd TestProject/node_modules/react-native-BGNativeModuleExample 
  2. $ git init . 
  3. $ git remote add origin https://github.com/liuchungui/react-native-BGNativeModuleExample.git 

2、我们需要创建原生模块的入口文件

我们需要在react-native-BGNativeModuleExample目录下创建一个index.js,它是整个原生模块的入口,我们这里只是将原生进行导出。

  1. //index.js 
  2. import React, { NativeModules } from 'react-native'
  3. module.exports = NativeModules.BGNativeModuleExample; 

3、发布到npm

在发布到npm之前,我们需要创建一个package.json文件,这个文件包含了module的所有信息,比如名称、版本、描述、依赖、作者、 license等。 我们在react-native-BGNativeModuleExample根目录下使用npm init命令来创建package.json,系统会提示我们输入所需的信息,不想输入的直接按下Enter跳过。

  1. $ npm init 
  2. This utility will walk you through creating a package.json file. 
  3. It only covers the most common items, and tries to guess sensible defaults. 
  4. See `npm help json` for definitive documentation on these fields 
  5. and exactly what they do
  6. Use `npm install  --save` afterwards to install a package and 
  7. save it as a dependency in the package.json file. 
  8. Press ^C at any time to quit. 
  9. name: (react-native-BGNativeModuleExample) 

输入完成之后,系统会要我们确认文件的内容是否有误,如果没有问题直接输入yes,那么package.json就创建好了。 我这里创建的package.json文件如下:

  1.   "name""react-native-nativemodule-example"
  2.   "version""1.0.0"
  3.   "description"""
  4.   "main""index.js"
  5.   "scripts": { 
  6.     "test""echo \"Error: no test specified\" && exit 1" 
  7.   }, 
  8.   "repository": { 
  9.     "type""git"
  10.     "url""git+https://github.com/liuchungui/react-native-BGNativeModuleExample.git" 
  11.   }, 
  12.   "author"""
  13.   "license""ISC"
  14.   "bugs": { 
  15.     "url""https://github.com/liuchungui/react-native-BGNativeModuleExample/issues" 
  16.   }, 
  17.   "homepage""https://github.com/liuchungui/react-native-BGNativeModuleExample#readme" 

如果我们编写的原生模块依赖于其他的原生模块,我们需要在package.json添加依赖关系,我们这里由于没有相关依赖,所以不需要添加:

  1. "dependencies": { 

初始化完package.json,我们就可以发布到npm上面了。

如果没有npm的账号,我们需要注册一个账号,这个账号会被添加到npm本地的配置中,用来发布module用。

  1. $ npm adduser 
  2. Username: your name 
  3. Password: your password 
  4. Email: yourmail@gmail.com 

成功之后,npm会把认证信息存储在~/.npmrc中,并且可以通过以下命令查看npm当前使用的用户:

  1. $ npm whoami 

以上完成之后,我们就可以进行发布了。

  1. $npm publish 
  2. + react-native-nativemodule-example@1.0.0 

到这里,我们已经成功把module发布到了npmjs.org。当然,我们也别忘记将我们的代码发布到github。

  1. $ git pull origin master 
  2. $ git add . 
  3. $ git commit -m 'add Project' 
  4. $ git push origin master 

有时候,有些文件没必要发布,例如Example文件,我们就可以通过.npmignore忽略它。例如我这里.npmignore文件内容如下:

  1. Example/ 
  2. .git 
  3. .gitignore 
  4. .idea 

这样的话,我们npm进行发布的时候,就不会将Example发布到npm上了。

4、添加Example,测试是否可用,添加README

我们在react-native-BGNativeModuleExample目录下创建一个Example的ReactNative工程,并且通 过rnpm install react-native-nativemodule-example命令安装我们发布的react-native-nativemodule- example模块。

  1. $ rnpm install react-native-nativemodule-example 
  2. TestProject@0.0.1 /Users/user/github/TestProject 
  3. └── react-native-nativemodule-example@1.0.0 
  4. rnpm-link info Linking react-native-nativemodule-example ios dependency 
  5. rnpm-link info iOS module react-native-nativemodule-example has been successfully linked 
  6. rnpm-link info Module react-native-nativemodule-example has been successfully installed & linked 

上面提示安装并且link成功,我们就可以在js中进行使用了。

  1. import BGNativeModuleExample from 'react-native-nativemodule-example'
  2. BGNativeModuleExample.testPrint("Jack", { 
  3.     height: '1.78m'
  4.     weight: '7kg' 
  5. }); 

5、我们在发布上线之后还需要编写README文件。

README文件是非常重要的,如果没有README文件,别人看到我们的原生组件,根本就不知道我们这个组件是用来干啥的。所以,我们很有必要添加一个README文件,这个文件需要告诉别人我们这个原生组件是干什么的、如何安装、API、使用手册等等。

6、原生模块升级,发布新版本

当我们添加新代码或者修复bug后,需要发布新的版本,我们只需要修改package.json文件中的version的值就行了,然后使用npm publish进行发布。

总结

本篇文章主要分成两个部分,一是讲述了编写原生模块的知识,二是将我们编写的内容发布到npm上。

参考

如何发布Node模块到NPM社区

原生模块

责任编辑:倪明 来源: 刘春桂的博客
相关推荐

2016-08-23 13:53:25

iOS开发逻辑

2016-12-22 19:53:46

AndroidAPPReactNative

2023-12-26 10:04:29

Electron应用开发框架

2011-08-03 13:43:50

iOS程序 打包 发布

2016-11-23 16:48:20

react-nativandroidjavascript

2014-07-23 13:17:53

iOSUITextField

2014-07-21 14:49:35

iOSUILabel

2015-09-22 09:50:36

FacebookAndroid

2015-03-30 12:13:23

React NativiOS

2011-08-15 11:13:06

IOS开发并发Dispatch Qu

2015-09-11 09:15:32

RyuSDN

2011-08-02 11:07:42

iOS开发 UIWebView

2017-02-15 09:25:36

iOS开发MQTT

2011-08-11 16:50:04

iOSTwitter

2015-10-10 16:02:36

React NativAndroid

2013-07-29 04:51:41

iOS开发iOS开发学习file列表查看

2013-04-11 16:08:50

iOS开发技巧积累

2013-07-25 15:15:26

iOS开发学习iOS全局变量

2014-08-08 10:12:44

IRC

2013-07-25 15:19:23

iOS开发学习Xcode打包framiOS开发
点赞
收藏

51CTO技术栈公众号