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

准备工作:

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

react native init TestProject

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

创建静态库,并将这个静态库手动链接到工程中
首先,我们在前面创建的ReactNative工程下的node_modules创建一个文件夹react-native-BGNativeModuleExample,然后我们在新创建的文件夹下再创建一个ios文件夹。

$ cd TestProject/node_modules
$ mkdir react-native-BGNativeModuleExample
$ cd react-native-BGNativeModuleExample
$ mkdir ios

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

iOS文件目录如下:

|____BGNativeModuleExample
| |____BGNativeModuleExample.h
| |____BGNativeModuleExample.m
|____BGNativeModuleExample.xcodeproj

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

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

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

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

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

一、编写原生模块代码

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

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

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

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

RCT_EXPORT_MODULE(BGNativeModuleExample);

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

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

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

2、为原生模块添加方法
我们需要明确的声明要给JS导出的方法,否则ReactNative不会导出任何方法。声明通过RCT_EXPORT_METHOD()宏来实现:

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

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

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

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。

RCT_EXPORT_METHOD(getNativeClass:(RCTResponseSenderBlock)callback) {
  callback(@[NSStringFromClass([self class])]);
}

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

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

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

5、Promises
原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的async/await语法则效果更佳。如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock,则对应的JS方法就会返回一个Promise对象。
我们通过Promises来实现原生模块是否会响应方法,响应则返回YES,不响应则返回一个错误信息,代码如下:

RCT_REMAP_METHOD(testRespondMethod,
                 name:(NSString *)name
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject) {
  if([self respondsToSelector:NSSelectorFromString(name)]) {
    resolve(@YES);
  }
  else {
    reject(@"-1001", @"not respond this method", nil);
  }
}

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

BGNativeModuleExample.testRespondMethod("dealloc")
    .then(result => {
      console.log("result is ", result);
    })
    .catch(error => {
      console.log(error);
    });

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

async testRespond() {
try {
  var result = BGNativeModuleExample.testRespondMethod("hell");
  if(result) {
    console.log("respond this method");
  }
} catch (e) {
  console.log(e);
}
  }

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

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

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方法,如下:

- (NSDictionary *)constantsToExport {
  return @{ @"BGModuleName" : @"BGNativeModuleExample",
            TestEventName: TestEventName
            };
}

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

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

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

8、给JS发送事件
即使没有被JS调用,本地模块也可以给JS发送事件通知。最直接的方式是使用eventDispatcher。
在这里,我们为了能够接收到事件,我们开一个定时器,每一秒发送一次事件。

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

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

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

注意: 编写OC代码时,需要添加@synthesize bridge = _bridge;,否则接收事件的时候就会报Exception -[BGNativeModuleExample brige]; unrecognized selector sent to instance的错误。
上面原生代码就编写好了,主要以代码实践为主,弥补官方文档中的一些不足,如果要需要了解更多的原生模块封装的知识,可以参考原生模块,也可以参考官方的源代码。

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

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

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

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

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

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

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

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

{  
"name": "react-native-nativemodule-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \\"Error: no test specified\\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+[https://github.com/liuchungui/react-native-BGNativeModuleExample.git](https://github.com/liuchungui/react-native-BGNativeModuleExample.git)"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "[https://github.com/liuchungui/react-native-BGNativeModuleExample/issues](https://github.com/liuchungui/react-native-BGNativeModuleExample/issues)"
},
"homepage": "[https://github.com/liuchungui/react-native-BGNativeModuleExample](https://github.com/liuchungui/react-native-BGNativeModuleExample)#readme"
}

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

"dependencies": {
}

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

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

$ npm adduser
Username: your name
Password: your password
Email: yourmail@gmail.com

这里需要注意的是填写Password时不会有任何显示,正常填写就可以了。

如果已经注册账号,则我们用npm login 登录即可

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

$ npm who am i

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

$npm publish
+ react-native-nativemodule-example@1.0.0

如果你以后修改了代码,然后想要同步到 npm 上的话请修改 package.json 中的 version 然后再次 publish,更新的版本上传的版本要大于上次。

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

$ git pull origin master
$ git add .
$ git commit -m 'add Project'
$ git push origin master

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

Example/
.git
.gitignore
.idea

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

4、添加Example,测试是否可用,添加README
我们在react-native-BGNativeModuleExample目录下创建一个Example的ReactNative工程,并且通过rnpm install react-native-nativemodule-example命令安装我们发布的react-native-nativemodule-example模块。

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

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

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

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

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

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

参考:
https://www.cnblogs.com/pingfan1990/p/4824658.html

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

推荐阅读更多精彩内容