前段时间做了一个ReactNativeApp,发现ReactNative里面很多组件是不存在的,所以还是需要写原生模块来进行JS调用,正是因为在写的时候过程中,遇到了很多问题,也发现了官网文档的很多不足之处。于是萌生了写一篇实用教程的想法,终于想出了这么一篇文章。整篇文章主要以编写原生模块为例,讲述了我们在编写原生模块时所用到的一些知识,并且在整个示例中,配备了完整的练习代码,方便大家理解和调试。除了这些内容之外,文章还介绍了我们如何将自己编写的原生模块发布到npm,以便与他人共享。希望对你有所帮助,也希望大家分享自己写的原生模块。示例代码github地址:https://github.com/liuchungui/react-native-BGNativeModuleExample准备工作:创建ReactNative项目我们需要先创建一个ReactNative项目,使用如下命令创建。reactnativeinitTestProject创建项目后,我们使用xcode打开TestProject/ios/下的iOS项目。创建一个静态库,并手动将这个静态库链接到项目中。首先我们在之前创建的ReactNative项目下的node_modules中创建一个文件夹react-native-BGNativeModuleExample,然后我们在新建的folder文件夹下创建一个ios文件。$cdTestProject/node_modules$mkdirreact-native-BGNativeModuleExample$cdreact-native-BGNativeModuleExample$mkdirios然后,因为ReactNative的组件都是静态库,如果我们发布到npm给别人使用,我们也需要构建静态库。我们使用Xcode创建一个名为BGNativeModuleExample的静态库。创建完成后,我们将创建的静态库中的所有文件复制到node_modules/react-native-BGNativeModuleExample/ios目录下。iOS文件目录如下:|____BGNativeModuleExample||____BGNativeModuleExample.h||____BGNativeModuleExample.m|____BGNativeModuleExample.xcodeproj最后我们需要手动链接这个静态库到工程中。1、用xcode打开创建好的静态库,添加一行HeaderSearchPaths,值为$(SRCROOT)/../../react-native/React,设置为recursive。2.将BGNativeModuleExample静态库项目拖到项目中的Library中。3.选择TARGETS=>TestProject=>BuildSettings=>LinkBinaryWithLibraries,在这里添加静态库libBGNativeModuleExample.a,我们的准备工作就完成了。我们这里的准备是有意的,就是模拟npm链接的过程,搭建环境,避免发布到npm后别人找不到静态库的问题。一、编写原生模块代码1、创建原生模块选择我们创建的BGNativeModuleExample静态库,然后在BGNativeModuleExample.h文件中导入RCTBridgeModule.h,使BGNativeModuleExample类遵循RCTBridgeModule协议。//BGNativeModuleExample.h文件内容如下#import#import"RCTBridgeModule.h"@interfaceBGNativeModuleExample:NSObject@end在BGNativeModuleExample.m文件中,我们需要实现RCTBridgeModule协议。为了实现RCTBridgeModule协议,我们的类需要包含RCT_EXPORT_MODULE()宏。这个宏也可以添加一个参数来指定在Javascript中访问这个模块的名称。如果未指定,则默认使用类名。在这里,我们将模块名称指定为BGNativeModuleExample。RCT_EXPORT_MODULE(BGNativeModuleExample);实现了RCTBridgeModule协议后,我们可以获取到我们在js中创建的原生模块,如下。import{NativeModules}from'react-native';varBGNativeModuleExample=NativeModules.BGNativeModuleExample;需要注意的是RCT_EXPORT_MODULE宏传递的参数在OC中不能是字符串。如果传了@"BGNativeModuleExample",那么我们导出到JS的模块名称其实就是@"BGNativeModuleExample",使用BGNativeModuleExample是找不到的。在这里,我们实际上可以通过打印NativeModules找到我们创建的原生模块。2.给原生模块添加方法,需要显式声明要导出到JS的方法,否则ReactNative不会导出任何方法。该语句是通过RCT_EXPORT_METHOD()宏实现的:RCT_EXPORT_METHOD(testPrint:(NSString*)nameinfo:(NSDictionary*)info){RCTLogInfo(@"%@:%@",name,info);}在JS中,我们可以执行此操作调用此方法:BGNativeModuleExample.testPrint("Jack",{height:'1.78m',weight:'7kg'});3、参数类型RCT_EXPORT_METHOD()支持所有标准的JSON类型,包括:字符串(NSString)数字(NSInteger,float,double,CGFloat,NSNumber)boolean(BOOL,NSNumber)数组(NSArray)包含这个listmap中的任意类型(NSDictionary)包含字符串类型键和此列表函数中的任何类型的值(RCTResponseSenderBlock)此外,还可以使用RCTConvert类支持的任何类型(有关更多信息,请参见RCTConvert)。RCTConvert还提供了一组辅助函数,用于获取JSON值并将其转换为本机Objective-C类型或类。更多信息,请点击原生模块。4.回调函数警告:本章内容还处于实验阶段,因为我们对回调函数的处理实践经验不多。回调函数在官方文档上面有警告,但是在使用过程中没有发现问题。在OC中,我们添加了一个getNativeClass方法,将当前模块的类名回调给JS。RCT_EXPORT_METHOD(getNativeClass:(RCTResponseSenderBlock)callback){callback(@[NSStringFromClass([selfclass])]);}在JS中,我们通过以下方式获取native模块的类名BGNativeModuleExample.getNativeClass(name=>{console.日志(“nativeClass:”,名称);});原生模块通常应该只调用一次回调函数。但是,他们可以保存回调并在将来调用它们。这在通过“委托函数”包装返回值的iOSAPI时最常见。5.Promises原生模块也可以使用promises来简化代码,ES2016(ES7)标准的async/await语法会更好用。如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock,对应的JS方法会返回一个Promise对象。我们使用Promises来实现native模块是否会响应该方法,如果响应则返回YES,如果不响应则返回错误信息。代码如下:RCT_REMAP_METHOD(testRespondMethod,name:(NSString*)nameresolver:(RCTPromiseResolveBlock)resolverejecter:(RCTPromiseRejectBlock)reject){if([selfrespondToSelector:NSSelectorFromString(name)]){resolve(@YES);}else{reject(@"-1001",@"notrespondthismethod",nil);}}在JS中,我们有两种调用方式,第一种方式是通过then....catch:BGNativeModuleExample.testRespondMethod("dealloc").then(result=>{console.log("resultis",result);}).catch(error=>{console.log(error);});第二种是通过try...catch调用的。与第一种相比,第二种会报警告“PossibleUnhandledPromissRejection(id:0)”。asynctestRespond(){try{varresult=BGNativeModuleExample.testRespondMethod("hell");if(result){console.log("respondthismethod");}}catch(e){console.log(e);}}注意:如果我们使用Promiss不需要参数,只需要删除OC中的name这一行即可;如果需要多个参数,只要在name下多加一行即可,注意中间不要加逗号。6、多线程我们这里操作的模块不涉及UI,所以专门创建了一个串行队列供其使用,如下:注意:分发队列是模块间共享的methodQueue方法会在模块初始化时执行一次,然后通过ReactNative的桥接机制保存,所以不需要自己保存队列的引用,除非你想在模块的其他地方使用它。但是,如果你想在几个模块之间共享同一个队列,你需要自己保存和返回同一个队列实例;仅返回具有相同名称的队列是行不通的。更多线程操作细节请参考:http://reactnative.cn/docs/0.24/native-modules-ios.html#content7。导出常量Native模块可以导出一些常量,可以在JavaScript端随时访问。使用这种方法传递一些静态数据可以避免通过桥来回交互。在OC中,我们实现constantsToExport方法,如下:-(NSDictionary*)constantsToExport{return@{@"BGModuleName":@"BGNativeModuleExample",TestEventName:TestEventName};}在JS中,我们打印这个常量console.log("BGModuleName值为“,BGNativeModuleExample.BGModuleName”;但是注意这个常量只在初始化的时候导出一次,所以即使你在运行时改变了constantToExport返回的值,也不会影响在JavaScript环境中得到的结果。8、向JS发送事件即使不被JS调用,本地模块也可以向JS发送事件通知。最直接的方法是使用eventDispatcher。在这里,为了接收事件,我们打开一个定时器,每秒发送一次事件。#import"BGNativeModuleExample.h"#import"RCTEventDispatcher.h"@implementationBGNativeModuleExample@synthesizebridge=_bridge;-(instancetype)init{if(self=[superinit]){[NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(senduserEventToJS):nilrepeats:YES];}returnsself;}-(void)receiveNotification:(NSNotification*)notification{[self.bridge.eventDispatchersendAppEventWithName:TestEventNamebody:@{@"name":@"Jack"}];}@end在JS中,我们收到类似NativeAppEventEmitter.addListener(BGNativeModuleExample.TestEventName,info=>{console.log(info);});的事件注意:在写OC代码的时候需要加上@synthesizebridge=_bridge;,否则接收事件时Exception-[BGNativeModuleExamplebrige];unrecognizedselectorsenttoinstance错误将被报告。以上原生代码编写,主要是基于代码实践,弥补官方文档的一些不足。如果想了解更多关于原生模块打包的知识,可以参考原生模块或者官方源码。2.发布上线我们按照上面的步骤编写完原生模块后,将我们编写的原生模块发布到npm。1.我们需要创建一个github仓库在github上创建一个仓库react-native-BGNativeModuleExample,然后关联到我们之前创建的react-native-BGNativeModuleExample目录$cdTestProject/node_modules/react-native-BGNativeModuleExample$gitinit.$gitremoteaddoriginhttps://github.com/liuchungui/react-native-BGNativeModuleExample.git2.我们需要创建native模块的入口文件。我们需要在react-native-BGNativeModuleExample目录下创建一个index.js,它是整个nativemodule的入口。这里我们只是把Export放在本地。//index.jsimportReact,{NativeModules}from'react-native';module.exports=NativeModules.BGNativeModuleExample;3.发布到npm在发布到npm之前,我们需要创建一个package.json文件,里面包含了模块的所有信息,比如名称、版本、描述、依赖、作者、许可等。我们在根目录下使用npminit命令react-native-BGNativeModuleExample的目录来创建package.json。系统会提示我们输入所需信息。如果不想输入,直接回车跳过即可。$npminitThisutilitywillwalkyouthroughcreatingapackage.jsonfile.Itonlycoversthemostcommonitems,andtriestoguesssensibledefaults.See`npmhelpjson`fordefinitivedocumentationonthesefieldsandexactlywhattheydo.Use`npminstall--save`afterwardstoinstallapackageandsaveitasadependencyinthepackage.jsonfile.Press^Catanytimetoquit.name:(react-native-BGNativeModuleExample)输入完成之后,系统会要我们确认文件的Whetherthecontentiswrong,ifthereisnoproblem,enteryesdirectly,thenpackage.jsonwillbecreated.我这里创建的package.json文件如下:{"name":"react-native-nativemodule-example","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo\"Error:notestspecified\"&&exit1"},"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"}如果我们编写的原生模块依赖于其他原生模块,我们需要将它们添加到包.json中以添加依赖项。由于我们这里没有相关的依赖,所以不需要添加:"dependencies":{}初始化package.json后,我们就可以发布到npm了。如果没有npm账号,我们需要注册一个账号,这个账号会添加到npm本地配置中发布模块。$npmadduserUsername:yournamePassword:yourpasswordEmail:yourmail@gmail.com成功后npm会将认证信息保存在~/.npmrc中,可以通过以下命令查看npm的当前用户:$npmwhoami以上完成后,我们可以继续宣布。$npmpublish+react-native-nativemodule-example@1.0.0在这里,我们已经成功地将模块发布到npmjs.org。当然,别忘了把我们的代码发布到github上。$gitpulloriginmaster$gitadd.$gitcommit-m'addProject'$gitpushoriginmaster有时候有些文件是不需要发布的,比如Example文件,我们可以通过.npmignore忽略。比如我这里的.npmignore文件内容是这样的:Example/.git.gitignore.idea这样的话,我们在发布npm的时候,是不会发布Example到npm的。4、添加Example,测试是否可用,添加README我们在react-native-BGNativeModuleExample目录下创建一个Example的ReactNative工程,通过rnpminstallreact-native-nativemodule安装我们发布的react-native-nativemodule-example-示例命令模块。$rnpminstallreact-native-nativemodule-exampleTestProject@0.0.1/Users/user/github/TestProject└──react-native-nativemodule-example@1.0.0rnpm-linkinfoLinkingreact-native-nativemodule-exampleiosdependencyrnpm-linkinfoOSmodulereact-native-nativepmmodule-examplefivessha-linkinfoModulereact-native-nativemodule-examplehasbeensuccessfulinstalled&linked上面提示安装成功,链接成功,所以我们可以在js中使用了。importBGNativeModuleExamplefrom'react-native-nativemodule-example';BGNativeModuleExample.testPrint("Jack",{height:'1.78m',weight:'7kg'});5.发布后我们需要编写README文件。README文件非常重要。如果没有README文件,别人看到我们的原生组件,就不知道我们的组件是干什么用的。因此,我们添加一个README文件是非常有必要的。这个文件需要告诉别人我们的原生组件是做什么的,如何安装,API,用户手册等。6.升级原生模块,发布新版本当我们添加新的代码或者修复bug时,我们需要发布一个新的版本。我们只需要修改package.json文件中version的值,然后使用npmpublish发布即可。小结本文主要分为两部分。一个是描述写nativemodules的知识,一个是把我们写的内容发布到npm上。参考如何发布Node模块到NPM社区原生模块
