NIUCLOUD是一款SaaS管理后台框架多应用插件+云编译。上千名开发者、服务商正在积极拥抱开发者生态。欢迎开发者们免费入驻。一起助力发展! 广告
### 原生模块的开发 * * * * * 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 ![](https://box.kancloud.cn/ceb4dfff7c26a4fd012ba908a8c6bead_1316x340.png) Ⅱ.选择Cocoa Touch Static Library ![](https://box.kancloud.cn/0ae8a25dfd4df3fd004ecb1e8b95c2c0_1308x940.png) ![](https://box.kancloud.cn/dabbcd22fda17345a0546218592166c2_1296x748.png) * 将该静态库拷贝到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。 ![](https://box.kancloud.cn/d5453214d0b88b1f23cb5b8ead89ffc6_1314x750.png) * 将react-native-nativeModuleTest/ios/nativeModuleTest/nativeModuleTest.xcodeproj手动拖到项目的Library中 ![](https://box.kancloud.cn/83493b31d57c5c566bd03b8762660fd4_1318x632.png) TARGETS - nativeModuleTest - Build Phases => Link Binary With Libraries,添加libnativeMoudleTest.a这个静态库 ![](https://box.kancloud.cn/062f6eb7ece9fcb1ea191381afb9d424_964x478.png) ※ 开始编写原生代码啦 在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) }) ~~~ 打印截图: ![](https://box.kancloud.cn/a38b547507cc9c0e8d317da46d630ca8_1300x958.png) #### 2. 解说 ※ 通信方式 所谓的通信其实就是js和oc之间是如何相互调用传参。一般有三种,优缺点如下。 ![](https://box.kancloud.cn/708d75946309ddec5a08acd02d1112d2_1324x282.png) 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