### 原生模块的开发
* * * * *
1.实战
* 准备过程,模拟npm链接(react-native link)
* 开始编写原生代码啦
2.解说
* 通信方式
* JS向原生模块传输数据
* 原生模块向JS传输数据
* 发送事件,在不被调用的情况下,原生模块自主向JS传输数据
3.发布上线
#### 1. 实战
※ 准备过程,模拟npm链接
a. 创建项目
~~~
react-native init nativeTest
~~~
b. 使用xcode打开nativeTest/ios/nativeTest.xcodeproj
c. 创建静态库,并将静态库链接到项目中
* 在nativeTest项目的node_modules中创建文件夹react-native-nativeModuleTest,再创建一个ios文件夹
~~~
cd nativeTest/node_modules
mkdir react-native-nativeModuleTest
cd react-native-nativeModuleTest
mkdir ios
~~~
* xcode建立静态库,名为nativeModuleTest
Ⅰ. File - New - Project

Ⅱ.选择Cocoa Touch Static Library


* 将该静态库拷贝到nativeTest/node_modules/react-native-nativeModuleTest/ios目录下
* xcode打开iOS目录下的nativeModuleTest.xcodeproj
* 添加Header Search Paths
TARGETS - nativeModuleTest - Search Paths
添加Header Search Paths,值为`$(SRCROOT)/../../react-native/React`,双击弹出设置为recursive。

* 将react-native-nativeModuleTest/ios/nativeModuleTest/nativeModuleTest.xcodeproj手动拖到项目的Library中

TARGETS - nativeModuleTest - Build Phases => Link Binary With Libraries,添加libnativeMoudleTest.a这个静态库

※ 开始编写原生代码啦
在React Native中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的Objective-C类,其中RCT是ReaCT的缩写。
`iOS小科普`:在ios中h和m后缀的文件区别
.h :头文件。头文件包含类,类型,函数和常数的声明。
.m :源代码文件。这是典型的源代码文件扩展名,可以包含Objective-C和C代码。
在源代码中包含头文件的时候,你可以使用标准的#include编译选项,Objective-C提供了更好的方法#import,#import选项和#include选项完全相同,只是它可以确保相同的文件只会被包含一次。Objective-C的例子和文档都倾向于使用#import。
a.打开Library中nativeModuleTest.h
实现一个nativeModuleTest类 ,继承RCTBridgeModule
~~~
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
@interface nativeModuleTest : NSObject <RCTBridgeModule>
@end
~~~
b.打开Library中nativeModuleTest.m
RCT_EXPORT_MODULE():暴露原生模块,参数可填写模块名,默认就会使用这个Objective-C类的名字
RCT_EXPORT_METHOD():暴露原生方法,可用在js中通过`模块名.方法`调用
~~~
#import "nativeModuleTest.h" // 包含头文件
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
@implementation nativeModuleTest : NSObject //必须加上父类 : NSObject
RCT_EXPORT_MODULE(nativeModuleTest);
//打印信息
RCT_EXPORT_METHOD(testPrint:(NSString *)name info:(NSDictionary *)info) {
RCTLogInfo(@"%@: %@", name, info);
}
static NSDictionary *DynamicDimensions() {
//当前屏幕尺寸
CGFloat width = MIN(RCTScreenSize().width,RCTScreenSize().height);
CGFloat height = MAX(RCTScreenSize().width,RCTScreenSize().height);
CGFloat scale = RCTScreenScale();
//横屏
if(UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)){
width = MAX(RCTScreenSize().width, RCTScreenSize().height);
height = MIN(RCTScreenSize().width, RCTScreenSize().height);
}
return @{@"width": @(width),
@"height": @(height),
@"scale": @(scale)
};
}
//获取屏幕尺寸callback方式
RCT_EXPORT_METHOD(getDynamicDimensions:(RCTResponseSenderBlock)callback) {
callback(@[[NSNull null], DynamicDimensions()]);
}
//获取屏幕尺寸promises方式
RCT_REMAP_METHOD(getDynamicDimensionsByPromise,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject){
NSArray *events = DynamicDimensions();
if (events) {
resolve(events);
} else {
reject(@"-1001", @"not respond this method", nil);
};
}
//屏幕方向监听
- (instancetype)init
{
self = [super init];
if(self){
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(orientationDidChange:)
name:UIDeviceOrientationDidChangeNotification object: nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
//通过RCTEventDispatcher将事件orientationDidChange通知到JavaScript
//如果我们需要从iOS原生方法发送数据到JavaScript中,那么使用eventDispatcher。首先我们需要在RCTBridgeModule实现中中引入: @synthesize bridge = _bridge;
@synthesize bridge = _bridge;
- (void)orientationDidChange:(id)noti
{
[_bridge.eventDispatcher sendDeviceEventWithName:
@"orientationDidChange" body:
@{ @"Orientation":UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? @"Landscape":@"Portrait",
@"Dimensions":DynamicDimensions()}];
}
// 将事件变量作为常量导出,以供JavaScript注册事件时使用
- (NSDictionary *)constantsToExport
{
return @{@"EVENT_ORIENTATION":@"orientationDidChange"};
}
@end
~~~
c.在js中调用
~~~
import { NativeModules,DeviceEventEmitter } from 'react-native';
console.log("原生模块:nativeModuleTest")
const nativeModuleTest = NativeModules.nativeModuleTest
console.log(nativeModuleTest)
//打印信息
nativeModuleTest.testPrint("Jack", {
height: '1.78m',
weight: '7kg'
});
//获取屏幕尺寸
nativeModuleTest.getDynamicDimensions((error, dimensions) => {
console.log("获取屏幕尺寸");
console.log(dimensions)
})
//屏幕旋转事件
let deviceChange = DeviceEventEmitter.addListener('orientationDidChange',(dimensions) => {
console.log("屏幕旋转,屏幕尺寸:");
console.log(dimensions)
})
~~~
打印截图:

#### 2. 解说
※ 通信方式
所谓的通信其实就是js和oc之间是如何相互调用传参。一般有三种,优缺点如下。

a. JS向原生模块传输数据: 只能调用一次
直接通过调用原生模块所暴露出来的接口,来为接口方法设置参数。
例子:
原生模块
~~~
//打印信息
RCT_EXPORT_METHOD(testPrint:(NSString *)name info:(NSDictionary *)info) {
RCTLogInfo(@"%@: %@", name, info);
}
~~~
JS调用
JS直接通过nativeModuleTest模块的testPrint方法传递参数
~~~
import { NativeModules} from 'react-native';
const nativeModuleTest = NativeModules.nativeModuleTest
//打印信息
nativeModuleTest.testPrint("Jack", {
height: '1.78m',
weight: '7kg'
});
~~~
b. 原生模块向JS传递数据:只能调用一次
借助Callbacks与Promises
[Callbacks]
Callbacks回调函数: 原生模块支持的一种特殊参数,它提供了一个函数来把返回值传回给JavaScript。
例子:
原生模块
~~~
static NSDictionary *DynamicDimensions() {
//当前屏幕尺寸
CGFloat width = MIN(RCTScreenSize().width,RCTScreenSize().height);
CGFloat height = MAX(RCTScreenSize().width,RCTScreenSize().height);
CGFloat scale = RCTScreenScale();
//横屏
if(UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)){
width = MAX(RCTScreenSize().width, RCTScreenSize().height);
height = MIN(RCTScreenSize().width, RCTScreenSize().height);
}
return @{@"width": @(width),
@"height": @(height),
@"scale": @(scale)
};
}
//获取屏幕尺寸
RCT_EXPORT_METHOD(getDynamicDimensions:(RCTResponseSenderBlock)callback) {
callback(@[[NSNull null], DynamicDimensions()]);
}
~~~
备注:
1.RCTResponseSenderBlock只接受一个参数——传递给JavaScript回调函数的参数数组。第一个参数是一个错误对象(没有发生错误的时候为null),而剩下的部分是函数的返回值。
2.原生模块通常只能调用回调函数一次。但是它可以保存callback在将来使用。
这在封装那些通过“委托函数”来获得返回值的iOS API时最为常见。
3.如果传递了回调函数,那么原生模块必须得调用它,不然会造成内存泄漏
JS调用
~~~
//获取屏幕尺寸
nativeModuleTest.getDynamicDimensions((error, dimensions) => {
if(error){
console.log(error);
}else {
console.log("获取屏幕尺寸");
console.log(dimensions)
}
})
~~~
【Promises】
原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的async/await语法则效果更佳。
如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock,则对应的JS方法就会返回一个Promise对象。
以下把上面的获取屏幕尺寸的方法用promises的方式再写一次
原生模块
~~~
//获取屏幕尺寸promises方式
RCT_REMAP_METHOD(getDynamicDimensionsByPromise,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject){
NSArray *events = DynamicDimensions();
if (events) {
resolve(events);
} else {
reject(@"-1001", @"not respond this method", nil);
};
}
~~~
JS调用
~~~
//方式1:获取屏幕尺寸promises方式:通过try...catch来调用
async function testRespond() {
try {
const dimensions = await nativeModuleTest.getDynamicDimensionsByPromise();
console.log("获取屏幕尺寸promises方式,通过try...catch来调用:");
console.log(dimensions)
} catch (e) {
console.error(e);
}
}
testRespond();
//方式2:获取屏幕尺寸promises方式:通过then....catch来调用
nativeModuleTest.getDynamicDimensionsByPromise()
.then(result => {
console.log("获取屏幕尺寸promises方式,通过then....catch来调用:");
console.log(result)
})
.catch(error => {
console.log(error);
});
~~~
c.原生模块向JS多次传递数据,即使原生模块没有被调用
应用:比如扫描二维码,每隔一段时间二维码会变化
核心:RCTEventDispatcher(原生模块和js之间的一个事件调度管理器。)
~~~
//屏幕方向监听
- (instancetype)init
{
self = [super init];
if(self){
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(orientationDidChange:)
name:UIDeviceOrientationDidChangeNotification object: nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
//通过RCTEventDispatcher将事件orientationDidChange通知到JavaScript
@synthesize bridge = _bridge;
- (void)orientationDidChange:(id)noti
{
[_bridge.eventDispatcher sendDeviceEventWithName:
@"orientationDidChange" body:
@{ @"Orientation":UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? @"Landscape":@"Portrait",
@"Dimensions":DynamicDimensions()}];
}
// 将事件变量作为常量导出,以供JavaScript注册事件时使用
- (NSDictionary *)constantsToExport
{
return @{@"EVENT_ORIENTATION":@"orientationDidChange"};
}
~~~
js调用
~~~
import { NativeModules,DeviceEventEmitter } from 'react-native';
// 屏幕旋转事件,创建一个包含你的模块的DeviceEventEmitter实例来订阅这些事件。
const subscription = DeviceEventEmitter.addListener('orientationDidChange',(dimensions) => {
console.log("屏幕旋转,屏幕尺寸:");
console.log(dimensions)
})
// 别忘了取消订阅,通常在componentWillUnmount生命周期方法中实现。
componentWillUnmount(){
subscription.remove();
}
~~~
#### 3. 发布上线
a. 创建GitHub仓库,react-native-nativeModuleTest
~~~
cd nativeTest/node_modules/react-native-nativeModuleTest
git init .
git remote add origin https://github.com/XXXXX/react-native-nativeModuleTest.git
~~~
b. 创建入口文件
~~~
//index.js
import React, { NativeModules } from 'react-native';
module.exports = NativeModules.nativeModuleTest;
~~~
c. 发布到npm
Ⅰ. 执行npm init 创建package.json文件
~~~
{
{
"name": "react-native-nativeModuleTest",
"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/XXXXXX/react-native-nativeModuleTest.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/XXXXX/react-native-nativeModuleTest/issues"
},
"homepage": "https://github.com/XXXXXX/react-native-nativeModuleTest#readme"
}
~~~
如果原生模块依赖于其他的原生模块,我们需要在package.json添加依赖关系
~~~
"dependencies": {
}
~~~
Ⅱ. 注册npm账号
这个账号会被添加到npm本地的配置中,用来发布module用
~~~
$ npm adduser
Username: name
Password: password
Email: mail@gmail.com
~~~
成功之后,npm会把认证信息存储在~/.npmrc中,通过以下命令可以查看npm当前使用的用户
~~~
$ npm whoami
~~~
Ⅲ. 发布啦
~~~
$npm publish
+ react-native-nativeModuleTest@1.0.0
~~~
参考资料:
链接原生库:http://reactnative.cn/docs/0.50/linking-libraries-ios.html
