React Native使用指南-原生模块
我們把React Native設(shè)計(jì)為可以在其基礎(chǔ)上編寫(xiě)真正的原生代碼,并且可以訪問(wèn)平臺(tái)所有的能力。這是一個(gè)相對(duì)高級(jí)的特性,我們并不認(rèn)為它應(yīng)當(dāng)在日常開(kāi)發(fā)的過(guò)程中經(jīng)常出現(xiàn),但具備這樣的能力是很重要的。如果React Native還不支持某個(gè)你需要的原生特性,你應(yīng)當(dāng)可以自己實(shí)現(xiàn)該特性的封裝。
本文是關(guān)于如何封裝原生模塊的高級(jí)向?qū)?#xff0c;我們假設(shè)您已經(jīng)具備Objective-C或者Swift,以及iOS核心庫(kù)(Foundation、UIKit)的相關(guān)知識(shí)。
iOS 日歷模塊演示
本向?qū)?huì)用iOS日歷API作為示例。我們的目標(biāo)就是在Javascript中可以訪問(wèn)到iOS的日歷功能。
在React Native中,一個(gè)“原生模塊”就是一個(gè)實(shí)現(xiàn)了“RCTBridgeModule”協(xié)議的Objective-C類,其中RCT是ReaCT的縮寫(xiě)。
// CalendarManager.h #import "RCTBridgeModule.h"@interface CalendarManager : NSObject <RCTBridgeModule> @end為了實(shí)現(xiàn)RCTBridgeModule協(xié)議,你的類需要包含RCT_EXPORT_MODULE()宏。這個(gè)宏也可以添加一個(gè)參數(shù)用來(lái)指定在Javascript中訪問(wèn)這個(gè)模塊的名字。如果你不指定,默認(rèn)就會(huì)使用這個(gè)Objective-C類的名字。
// CalendarManager.m @implementation CalendarManagerRCT_EXPORT_MODULE();@end你必須明確的聲明要給Javascript導(dǎo)出的方法,否則React Native不會(huì)導(dǎo)出任何方法。聲明通過(guò)RCT_EXPORT_METHOD()宏來(lái)實(shí)現(xiàn):
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) {RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); }現(xiàn)在從Javascript里可以這樣調(diào)用這個(gè)方法:
var CalendarManager = require('react-native').NativeModules.CalendarManager; CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');注意: Javascript方法名
導(dǎo)出到Javascript的方法名是Objective-C的方法名的第一個(gè)部分。React Native還定義了一個(gè)RCT_REMAP_METHOD()宏,它可以指定Javascript方法名。當(dāng)許多方法的第一部分相同的時(shí)候用它來(lái)避免在Javascript端的名字沖突。
橋接到Javascript的方法返回值類型必須是void。React Native的橋接操作是異步的,所以要返回結(jié)果給Javascript,你必須通過(guò)回調(diào)或者觸發(fā)事件來(lái)進(jìn)行。(參見(jiàn)本文檔后面的部分)
參數(shù)類型
RCT_EXPORT_METHOD?支持所有標(biāo)準(zhǔn)JSON類型,包括:
- string (NSString)
- number (NSInteger,?float,?double,?CGFloat,?NSNumber)
- boolean (BOOL,?NSNumber)
- array (NSArray) 包含本列表中任意類型
- map (NSDictionary) 包含string類型的鍵和本列表中任意類型的值
- function (RCTResponseSenderBlock)
除此以外,任何RCTConvert類支持的的類型也都可以使用(參見(jiàn)RCTConvert了解更多信息)。RCTConvert還提供了一系列輔助函數(shù),用來(lái)接收一個(gè)JSON值并轉(zhuǎn)換到原生Objective-C類型或類。
在我們的CalendarManager例子里,我們需要把事件的時(shí)間交給原生方法。我們不能在橋接通道里傳遞Date對(duì)象,所以需要把日期轉(zhuǎn)化成字符串或數(shù)字來(lái)傳遞。我們可以這么實(shí)現(xiàn)原生函數(shù):
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSNumber *)secondsSinceUnixEpoch) {NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch]; }或者這樣:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString) {NSDate *date = [RCTConvert NSDate:ISO8601DateString]; }不過(guò)我們可以依靠自動(dòng)類型轉(zhuǎn)換的特性,跳過(guò)手動(dòng)的類型轉(zhuǎn)換,而直接這么寫(xiě):
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date) {// Date is ready to use! }在Javascript既可以這樣:
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.getTime()); // 把日期以u(píng)nix時(shí)間戳形式傳遞也可以這樣:
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.toISOString()); // 把日期以ISO-8601的字符串形式傳遞兩個(gè)值都會(huì)被轉(zhuǎn)換為正確的NSDate類型。但如果提供一個(gè)不合法的值,譬如一個(gè)Array,則會(huì)產(chǎn)生一個(gè)“紅屏”報(bào)錯(cuò)信息。
隨著CalendarManager.addEvent方法變得越來(lái)越復(fù)雜,參數(shù)的個(gè)數(shù)越來(lái)越多,其中有一些可能是可選的參數(shù)。在這種情況下我們應(yīng)該考慮修改我們的API,用一個(gè)dictionary來(lái)存放所有的事件參數(shù),像這樣:
#import "RCTConvert.h"RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details) {NSString *location = [RCTConvert NSString:details[@"location"]];NSDate *time = [RCTConvert NSDate:details[@"time"]];... }然后在JS里這樣調(diào)用:
CalendarManager.addEvent('Birthday Party', {location: '4 Privet Drive, Surrey',time: date.toTime(),description: '...' })注意: 關(guān)于數(shù)組和映射
Objective-C并沒(méi)有提供確保這些結(jié)構(gòu)體內(nèi)部值的類型的方式。你的原生模塊可能希望收到一個(gè)字符串?dāng)?shù)組,但如果JavaScript在調(diào)用的時(shí)候提供了一個(gè)混合number和string的數(shù)組,你會(huì)收到一個(gè)NSArray,里面既有NSNumber也有NSString。對(duì)于數(shù)組來(lái)說(shuō),RCTConvert提供了一些類型化的集合,譬如NSStringArray或者UIColorArray,你可以用在你的函數(shù)聲明中。對(duì)于映射而言,開(kāi)發(fā)者有責(zé)任自己調(diào)用RCTConvert的輔助方法來(lái)檢測(cè)和轉(zhuǎn)換值的類型。
回調(diào)函數(shù)
警告
本章節(jié)內(nèi)容目前還處在實(shí)驗(yàn)階段,因?yàn)槲覀冞€并沒(méi)有太多的實(shí)踐經(jīng)驗(yàn)來(lái)處理回調(diào)函數(shù)。
原生模塊還支持一種特殊的參數(shù)——回調(diào)函數(shù)。它提供了一個(gè)函數(shù)來(lái)把返回值傳回給JavaScript。
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback) {NSArray *events = ...callback(@[[NSNull null], events]); }RCTResponseSenderBlock只接受一個(gè)參數(shù)——傳遞給JavaScript回調(diào)函數(shù)的參數(shù)數(shù)組。在上面這個(gè)例子里我們用Node.js的常用習(xí)慣:第一個(gè)參數(shù)是一個(gè)錯(cuò)誤對(duì)象(沒(méi)有發(fā)生錯(cuò)誤的時(shí)候?yàn)閚ull),而剩下的部分是函數(shù)的返回值。
CalendarManager.findEvents((error, events) => {if (error) {console.error(error);} else {this.setState({events: events});} })原生模塊通常只應(yīng)調(diào)用回調(diào)函數(shù)一次。但是,它可以保存callback并在將來(lái)調(diào)用。這在封裝那些通過(guò)“委托函數(shù)”來(lái)獲得返回值的iOS API時(shí)最為常見(jiàn)。RCTAlertManager中就屬于這種情況。
如果你想傳遞一個(gè)更接近Error類型的對(duì)象給Javascript,可以用RCTUtils.h提供的RCTMakeError函數(shù)。現(xiàn)在它僅僅是發(fā)送了一個(gè)和Error結(jié)構(gòu)一樣的dictionary給Javascript,但我們考慮在將來(lái)版本里讓它產(chǎn)生一個(gè)真正的Error對(duì)象。
Promises
譯注:這一部分涉及到較新的js語(yǔ)法和特性,不熟悉的讀者建議先閱讀ES6的相關(guān)書(shū)籍和文檔。
原生模塊還可以使用promise來(lái)簡(jiǎn)化代碼,搭配ES2016(ES7)標(biāo)準(zhǔn)的async/await語(yǔ)法則效果更佳。如果橋接原生方法的最后兩個(gè)參數(shù)是RCTPromiseResolveBlock和RCTPromiseRejectBlock,則對(duì)應(yīng)的JS方法就會(huì)返回一個(gè)Promise對(duì)象。
我們把上面的代碼用promise來(lái)代替回調(diào)進(jìn)行重構(gòu):
RCT_REMAP_METHOD(findEvents,resolver:(RCTPromiseResolveBlock)resolverejecter:(RCTPromiseRejectBlock)reject)) {NSArray *events = ...if (events) {resolve(events);} else {reject(error);} }現(xiàn)在JavaScript端的方法會(huì)返回一個(gè)Promise。這樣你就可以在一個(gè)聲明了async的異步函數(shù)內(nèi)使用await關(guān)鍵字來(lái)調(diào)用,并等待其結(jié)果返回。(雖然這樣寫(xiě)著看起來(lái)像同步操作,但實(shí)際仍然是異步的,并不會(huì)阻塞執(zhí)行來(lái)等待)。
async function updateEvents() {try {var events = await CalendarManager.findEvents();this.setState({ events });} catch (e) {console.error(e);} }updateEvents();多線程
原生模塊不應(yīng)對(duì)自己被調(diào)用時(shí)所處的線程做任何假設(shè)。React Native在一個(gè)獨(dú)立的串行GCD隊(duì)列中調(diào)用原生模塊的方法,但這屬于實(shí)現(xiàn)的細(xì)節(jié),并且可能會(huì)在將來(lái)的版本中改變。通過(guò)實(shí)現(xiàn)方法- (dispatch_queue_t)methodQueue,原生模塊可以指定自己想在哪個(gè)隊(duì)列中被執(zhí)行。具體來(lái)說(shuō),如果模塊需要調(diào)用一些必須在主線程才能使用的API,那應(yīng)當(dāng)這樣指定:
- (dispatch_queue_t)methodQueue {return dispatch_get_main_queue(); }類似的,如果一個(gè)操作需要花費(fèi)很長(zhǎng)時(shí)間,原生模塊不應(yīng)該阻塞住,而是應(yīng)當(dāng)聲明一個(gè)用于執(zhí)行操作的獨(dú)立隊(duì)列。舉個(gè)例子,RCTAsyncLocalStorage模塊創(chuàng)建了自己的一個(gè)queue,這樣它在做一些較慢的磁盤(pán)操作的時(shí)候就不會(huì)阻塞住React本身的消息隊(duì)列:
- (dispatch_queue_t)methodQueue {return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); }指定的methodQueue會(huì)被你模塊里的所有方法共享。如果你的方法中“只有一個(gè)”是耗時(shí)較長(zhǎng)的(或者是由于某種原因必須在不同的隊(duì)列中運(yùn)行的),你可以在函數(shù)體內(nèi)用dispatch_async方法來(lái)在另一個(gè)隊(duì)列執(zhí)行,而不影響其他方法:
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// 在這里執(zhí)行長(zhǎng)時(shí)間的操作...// 你可以在任何線程/隊(duì)列中執(zhí)行回調(diào)函數(shù)callback(@[...]);}); }注意: 在模塊之間共享分發(fā)隊(duì)列
methodQueue方法會(huì)在模塊被初始化的時(shí)候被執(zhí)行一次,然后會(huì)被React Native的橋接機(jī)制保存下來(lái),所以你不需要自己保存隊(duì)列的引用,除非你希望在模塊的其它地方使用它。但是,如果你希望在若干個(gè)模塊中共享同一個(gè)隊(duì)列,則需要自己保存并返回相同的隊(duì)列實(shí)例;僅僅是返回相同名字的隊(duì)列是不行的。
導(dǎo)出常量
原生模塊可以導(dǎo)出一些常量,這些常量在JavaScript端隨時(shí)都可以訪問(wèn)。用這種方法來(lái)傳遞一些靜態(tài)數(shù)據(jù),可以避免通過(guò)bridge進(jìn)行一次來(lái)回交互。
- (NSDictionary *)constantsToExport {return @{ @"firstDayOfTheWeek": @"Monday" }; }Javascript端可以隨時(shí)同步地訪問(wèn)這個(gè)數(shù)據(jù):
console.log(CalendarManager.firstDayOfTheWeek);但是注意這個(gè)常量?jī)H僅在初始化的時(shí)候?qū)С隽艘淮?#xff0c;所以即使你在運(yùn)行期間改變constantToExport返回的值,也不會(huì)影響到JavaScript環(huán)境下所得到的結(jié)果。
枚舉常量
用NS_ENUM定義的枚舉類型必須要先擴(kuò)展對(duì)應(yīng)的RCTConvert方法才可以作為函數(shù)參數(shù)傳遞。
假設(shè)我們要導(dǎo)出如下的NS_ENUM定義:
typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {UIStatusBarAnimationNone,UIStatusBarAnimationFade,UIStatusBarAnimationSlide, };你需要這樣來(lái)擴(kuò)展RCTConvert類:
@implementation RCTConvert (StatusBarAnimation)RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)},UIStatusBarAnimationNone, integerValue) @end接著你可以這樣定義方法并且導(dǎo)出enum值作為常量:
- (NSDictionary *)constantsToExport {return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) } };RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animationcompletion:(RCTResponseSenderBlock)callback)你的枚舉現(xiàn)在會(huì)用上面提供的選擇器進(jìn)行轉(zhuǎn)換(上面的例子中是integerValue),然后再傳遞給你導(dǎo)出的函數(shù)。
給Javascript發(fā)送事件
即使沒(méi)有被JavaScript調(diào)用,本地模塊也可以給JavaScript發(fā)送事件通知。最直接的方式是使用eventDispatcher:
#import "RCTBridge.h" #import "RCTEventDispatcher.h"@implementation CalendarManager@synthesize bridge = _bridge;- (void)calendarEventReminderReceived:(NSNotification *)notification {NSString *eventName = notification.userInfo[@"name"];[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"body:@{@"name": eventName}]; }@end在JavaScript中可以這樣訂閱事件:
var { NativeAppEventEmitter } = require('react-native');var subscription = NativeAppEventEmitter.addListener('EventReminder',(reminder) => console.log(reminder.name) ); ... // 千萬(wàn)不要忘記忘記取消訂閱, 通常在componentWillUnmount函數(shù)中實(shí)現(xiàn)。 subscription.remove();更多的給JavaScript發(fā)送事件的例子,參見(jiàn)RCTLocationObserver.
從Swift導(dǎo)出
Swift不支持宏,所以從Swift向React Native導(dǎo)出類和函數(shù)需要多做一些設(shè)置,但是大致與Objective-C是相同的。
假設(shè)我們已經(jīng)有了一個(gè)一樣的CalendarManager,不過(guò)是用Swift實(shí)現(xiàn)的類:
// CalendarManager.swift@objc(CalendarManager) class CalendarManager: NSObject {@objc func addEvent(name: String, location: String, date: NSNumber) -> Void {// Date is ready to use!}}注意: 你必須使用@objc標(biāo)記來(lái)確保類和函數(shù)對(duì)Objective-C公開(kāi)。
接著,創(chuàng)建一個(gè)私有的實(shí)現(xiàn)文件,并將必要的信息注冊(cè)到React Native中。
// CalendarManagerBridge.m #import "RCTBridgeModule.h"@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)@end請(qǐng)注意,一旦你在IOS中混用2種語(yǔ)言, 你還需要一個(gè)額外的橋接頭文件,稱作“bridging header”,用來(lái)導(dǎo)出Objective-C文件給Swift。如果你是通過(guò)Xcode菜單中的File>New File來(lái)創(chuàng)建的Swift文件,Xcode會(huì)自動(dòng)為你創(chuàng)建這個(gè)頭文件。在這個(gè)頭文件中,你需要引入RCTBridgeModule.h。
// CalendarManager-Bridging-Header.h #import "RCTBridgeModule.h"同樣的,你也可以使用RCT_EXTERN_REMAP_MODULE和RCT_EXTERN_REMAP_METHOD來(lái)改變導(dǎo)出模塊和方法的JavaScript調(diào)用名稱。 了解更多信息,請(qǐng)參閱RCTBridgeModule.
總結(jié)
以上是生活随笔為你收集整理的React Native使用指南-原生模块的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: spring-boot发送邮件失败 Au
- 下一篇: [记录] --- linux安装redi