ReactNative之原生模塊開發(fā)并發(fā)布--iOS篇
前段時間做了個ReactNative的App,發(fā)現(xiàn)ReactNative中不少組件并不存在,所以還是需要自己對原生模塊進行編寫讓JS調(diào)用, 正是因為在這個編寫過程中遇到不少問題,發(fā)覺了官網(wǎng)文檔中許多的不足。所以產(chǎn)生了寫一個實踐教程的想法,最終有了這么一篇文章。
整篇文章主要以編寫一個原生模塊為例子,來講述了我們在編寫原生模塊所用到的一些知識,并且在整個例子中,配有了完整的實踐代碼,方便大家理解并調(diào) 試。除了這些內(nèi)容,文章還講述了我們?nèi)绾螌⒆约壕帉懙脑K發(fā)布到npm上分享給別人使用。希望能夠給大家?guī)韼椭?,也希望大家將自己編寫的原生模塊分 享出來。
示例代碼github地址:https://github.com/liuchungui/react-native-BGNativeModuleExample
準備工作:
創(chuàng)建ReactNative工程
我們需要先創(chuàng)建一個ReactNative工程,使用如下命令創(chuàng)建。
- react native init TestProject
創(chuàng)建好工程之后,我們使用xcode打開TestProject/ios/下的iOS工程。
創(chuàng)建靜態(tài)庫,并將這個靜態(tài)庫手動鏈接到工程中
首先,我們在前面創(chuàng)建的ReactNative工程下的node_modules創(chuàng)建一個文件夾react-native-BGNativeModuleExample,然后我們在新創(chuàng)建的文件夾下再創(chuàng)建一個ios文件夾。
- $ cd TestProject/node_modules
- $ mkdir react-native-BGNativeModuleExample
- $ cd react-native-BGNativeModuleExample
- $ mkdir ios
然后,由于ReactNative的組件都是一個個靜態(tài)庫,我們發(fā)布到npm給別人使用的話,也需要建立靜態(tài)庫。我們使用Xcode建立靜態(tài)庫,取 名為BGNativeModuleExample。建立之后,我們將創(chuàng)建的靜態(tài)庫中的文件全部copy到node_modules/react- native-BGNativeModuleExample/ios目錄下。
iOS文件目錄如下:
- |____BGNativeModuleExample
- | |____BGNativeModuleExample.h
- | |____BGNativeModuleExample.m
- |____BGNativeModuleExample.xcodeproj
最后,我們需要手動將這個靜態(tài)庫鏈接到工程中。
1、使用xcode打開創(chuàng)建的靜態(tài)庫,添加一行Header Search Paths,值為$(SRCROOT)/../../react-native/React,并設置為recursive。
2、將BGNativeModuleExample靜態(tài)庫工程拖動到工程中的Library中。
3、選中 TARGETS => TestProject => Build Settings => Link Binary With Libraries,添加libBGNativeModuleExample.a這個靜態(tài)庫
到此,我們準備工作完成了。我們這里這么準備是有用意的,那就是模擬npm鏈接的過程,建立好了環(huán)境,避免了發(fā)布到npm上后別人使用找不到靜態(tài)庫的問題。
一、編寫原生模塊代碼
1、創(chuàng)建原生模塊
選中我們創(chuàng)建的BGNativeModuleExample靜態(tài)庫,然后在BGNativeModuleExample.h文件中導入RCTBridgeModule.h,讓BGNativeModuleExample類遵循RCTBridgeModule協(xié)議。
- //BGNativeModuleExample.h文件的內(nèi)容如下
- #import #import "RCTBridgeModule.h"
- @interface BGNativeModuleExample : NSObject @end
在BGNativeModuleExample.m文件中,我們需要實現(xiàn)RCTBridgeModule協(xié)議。為了實現(xiàn) RCTBridgeModule協(xié)議,我們的類需要包含RCT_EXPORT_MODULE()宏。這個宏也可以添加一個參數(shù)用來指定在 Javascript中訪問這個模塊的名字。如果不指定,默認會使用這個類的名字。
在這里,我們指定了模塊的名字為BGNativeModuleExample。
- RCT_EXPORT_MODULE(BGNativeModuleExample);
實現(xiàn)了RCTBridgeModule協(xié)議之后,我們就可以在js中如下獲取到我們創(chuàng)建的原生模塊。
- import { NativeModules } from 'react-native';
- var BGNativeModuleExample = NativeModules.BGNativeModuleExample;
需要注意的是,RCT_EXPORT_MODULE宏傳遞的參數(shù)不能是OC中的字符串。如果傳遞 @“BGNativeModuleExample",那么我們導出給JS的模塊名字其實是@"BGNativeModuleExample",使用 BGNativeModuleExample就找不到了。在這里,我們其實可以通過打印NativeModules來查找到我們創(chuàng)建的原生模塊。
2、為原生模塊添加方法
我們需要明確的聲明要給JS導出的方法,否則ReactNative不會導出任何方法。聲明通過RCT_EXPORT_METHOD()宏來實現(xiàn):
- RCT_EXPORT_METHOD(testPrint:(NSString *)name info:(NSDictionary *)info) {
- RCTLogInfo(@"%@: %@", name, info);
- }
在JS中,我們可以這樣調(diào)用這個方法:
- BGNativeModuleExample.testPrint("Jack", {
- height: '1.78m',
- weight: '7kg'
- });
3、參數(shù)類型
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還提供了一系列輔助函數(shù),用來接收一個JSON值并轉換到原生Objective-C類型或類。
了解更多請點擊原生模塊。
4、回調(diào)函數(shù)
警告:本章節(jié)內(nèi)容目前還處在實驗階段,因為我們還并沒有太多的實踐經(jīng)驗來處理回調(diào)函數(shù)。
回調(diào)函數(shù),在官方的文檔中是有上面的一個警告,不過在使用過程暫時未發(fā)現(xiàn)問題。在OC中,我們添加一個getNativeClass方法,將當前模塊的類名回調(diào)給JS。
- RCT_EXPORT_METHOD(getNativeClass:(RCTResponseSenderBlock)callback) {
- callback(@[NSStringFromClass([self class])]);
- }
在JS中,我們通過以下方式獲取到原生模塊的類名
- BGNativeModuleExample.getNativeClass(name => {
- console.log("nativeClass: ", name);
- });
原生模塊通常只應調(diào)用回調(diào)函數(shù)一次。但是,它們可以保存callback并在將來調(diào)用。這在封裝那些通過“委托函數(shù)”來獲得返回值的iOS API時最常見。
5、Promises
原生模塊還可以使用promise來簡化代碼,搭配ES2016(ES7)標準的async/await語法則效果更佳。如果橋接原生方法的最后兩 個參數(shù)是RCTPromiseResolveBlock和RCTPromiseRejectBlock,則對應的JS方法就會返回一個Promise對 象。
我們通過Promises來實現(xiàn)原生模塊是否會響應方法,響應則返回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中,我們有兩種方式調(diào)用,第一種是通過then....catch的方式:
- BGNativeModuleExample.testRespondMethod("dealloc")
- .then(result => {
- console.log("result is ", result);
- })
- .catch(error => {
- console.log(error);
- });
第二種是通過try...catch來調(diào)用,與第一種相比,第二種會報警告”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我們不需要參數(shù),則在OC去掉name那一行就行了;如果需要多個參數(shù),在name下面多加一行就行了,注意它們之間不需要添加逗號。
6、多線程
我們這里操作的模塊沒有涉及到UI,所以專門建立一個串行的隊列給它使用,如下:
- return dispatch_queue_create("com.liuchungui.demo", DISPATCH_QUEUE_SERIAL);
注意: 在模塊之間共享分發(fā)隊列
methodQueue方法會在模塊被初始化的時候被執(zhí)行一次,然后會被React Native的橋接機制保存下來,所以你不需要自己保存隊列的引用,除非你希望在模塊的其它地方使用它。但是,如果你希望在若干個模塊中共享同一個隊列, 則需要自己保存并返回相同的隊列實例;僅僅是返回相同名字的隊列是不行的。
更多線程的操作細節(jié)可以參考:http://reactnative.cn/docs/0.24/native-modules-ios.html#content
7、導出常量
原生模塊可以導出一些常量,這些常量在JavaScript端隨時都可以訪問。用這種方法來傳遞一些靜態(tài)數(shù)據(jù),可以避免通過bridge進行一次來回交互。
OC中,我們實現(xiàn)constantsToExport方法,如下:
- - (NSDictionary *)constantsToExport {
- return @{ @"BGModuleName" : @"BGNativeModuleExample",
- TestEventName: TestEventName
- };
- }
JS中,我們打印一下這個常量
- console.log("BGModuleName value is ", BGNativeModuleExample.BGModuleName);
但是注意這個常量僅僅在初始化的時候導出了一次,所以即使你在運行期間改變constantToExport返回的值,也不會影響到JavaScript環(huán)境下所得到的結果。
8、給JS發(fā)送事件
即使沒有被JS調(diào)用,本地模塊也可以給JS發(fā)送事件通知。最直接的方式是使用eventDispatcher。
在這里,我們?yōu)榱四軌蚪邮盏绞录?,我們開一個定時器,每一秒發(fā)送一次事件。
- #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的錯誤。
上面原生代碼就編寫好了,主要以代碼實踐為主,彌補官方文檔中的一些不足,如果要需要了解更多的原生模塊封裝的知識,可以參考原生模塊,也可以參考官方的源代碼。
二、發(fā)布上線
我們按照上面步驟編寫好原生模塊之后,接下來將我們寫的原生模塊發(fā)布到npm。
1、我們需要創(chuàng)建github倉庫
在github上創(chuàng)建一個倉庫react-native-BGNativeModuleExample,然后關聯(lián)到我們前面創(chuàng)建的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、我們需要創(chuàng)建原生模塊的入口文件
我們需要在react-native-BGNativeModuleExample目錄下創(chuàng)建一個index.js,它是整個原生模塊的入口,我們這里只是將原生進行導出。
- //index.js
- import React, { NativeModules } from 'react-native';
- module.exports = NativeModules.BGNativeModuleExample;
3、發(fā)布到npm
在發(fā)布到npm之前,我們需要創(chuàng)建一個package.json文件,這個文件包含了module的所有信息,比如名稱、版本、描述、依賴、作者、 license等。 我們在react-native-BGNativeModuleExample根目錄下使用npm init命令來創(chuàng)建package.json,系統(tǒng)會提示我們輸入所需的信息,不想輸入的直接按下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)
輸入完成之后,系統(tǒng)會要我們確認文件的內(nèi)容是否有誤,如果沒有問題直接輸入yes,那么package.json就創(chuàng)建好了。 我這里創(chuàng)建的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"
- },
- "author": "",
- "license": "ISC",
- "bugs": {
- "url": "https://github.com/liuchungui/react-native-BGNativeModuleExample/issues"
- },
- "homepage": "https://github.com/liuchungui/react-native-BGNativeModuleExample#readme"
- }
如果我們編寫的原生模塊依賴于其他的原生模塊,我們需要在package.json添加依賴關系,我們這里由于沒有相關依賴,所以不需要添加:
- "dependencies": {
- }
初始化完package.json,我們就可以發(fā)布到npm上面了。
如果沒有npm的賬號,我們需要注冊一個賬號,這個賬號會被添加到npm本地的配置中,用來發(fā)布module用。
- $ npm adduser
- Username: your name
- Password: your password
- Email: yourmail@gmail.com
成功之后,npm會把認證信息存儲在~/.npmrc中,并且可以通過以下命令查看npm當前使用的用戶:
- $ npm whoami
以上完成之后,我們就可以進行發(fā)布了。
- $npm publish
- + react-native-nativemodule-example@1.0.0
到這里,我們已經(jīng)成功把module發(fā)布到了npmjs.org。當然,我們也別忘記將我們的代碼發(fā)布到github。
- $ git pull origin master
- $ git add .
- $ git commit -m 'add Project'
- $ git push origin master
有時候,有些文件沒必要發(fā)布,例如Example文件,我們就可以通過.npmignore忽略它。例如我這里.npmignore文件內(nèi)容如下:
- Example/
- .git
- .gitignore
- .idea
這樣的話,我們npm進行發(fā)布的時候,就不會將Example發(fā)布到npm上了。
4、添加Example,測試是否可用,添加README
我們在react-native-BGNativeModuleExample目錄下創(chuàng)建一個Example的ReactNative工程,并且通 過rnpm install react-native-nativemodule-example命令安裝我們發(fā)布的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、我們在發(fā)布上線之后還需要編寫README文件。
README文件是非常重要的,如果沒有README文件,別人看到我們的原生組件,根本就不知道我們這個組件是用來干啥的。所以,我們很有必要添加一個README文件,這個文件需要告訴別人我們這個原生組件是干什么的、如何安裝、API、使用手冊等等。
6、原生模塊升級,發(fā)布新版本
當我們添加新代碼或者修復bug后,需要發(fā)布新的版本,我們只需要修改package.json文件中的version的值就行了,然后使用npm publish進行發(fā)布。
總結
本篇文章主要分成兩個部分,一是講述了編寫原生模塊的知識,二是將我們編寫的內(nèi)容發(fā)布到npm上。
參考