React Native使用指南-原生模組
我們把React Native設計為可以在其基礎上編寫真正的原生程式碼,並且可以訪問平臺所有的能力。這是一個相對高階的特性,我們並不認為它應當在日常開發的過程中經常出現,但具備這樣的能力是很重要的。如果React Native還不支援某個你需要的原生特性,你應當可以自己實現該特性的封裝。
本文是關於如何封裝原生模組的高階嚮導,我們假設您已經具備Objective-C或者Swift,以及iOS核心庫(Foundation、UIKit)的相關知識。
iOS 日曆模組演示
本嚮導將會用iOS日曆API作為示例。我們的目標就是在Javascript中可以訪問到iOS的日曆功能。
在React Native中,一個“原生模組”就是一個實現了“RCTBridgeModule”協議的Objective-C類,其中RCT是ReaCT的縮寫。
// CalendarManager.h
#import "RCTBridgeModule.h"
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
為了實現RCTBridgeModule
協議,你的類需要包含RCT_EXPORT_MODULE()
// CalendarManager.m
@implementation CalendarManager
RCT_EXPORT_MODULE();
@end
你必須明確的宣告要給Javascript匯出的方法,否則React Native不會匯出任何方法。宣告通過RCT_EXPORT_METHOD()
巨集來實現:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location )
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
現在從Javascript裡可以這樣呼叫這個方法:
var CalendarManager = require('react-native').NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');
注意: Javascript方法名
匯出到Javascript的方法名是Objective-C的方法名的第一個部分。React Native還定義了一個
RCT_REMAP_METHOD()
巨集,它可以指定Javascript方法名。當許多方法的第一部分相同的時候用它來避免在Javascript端的名字衝突。
橋接到Javascript的方法返回值型別必須是void
。React Native的橋接操作是非同步的,所以要返回結果給Javascript,你必須通過回撥或者觸發事件來進行。(參見本文件後面的部分)
引數型別
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
還提供了一系列輔助函式,用來接收一個JSON值並轉換到原生Objective-C型別或類。
在我們的CalendarManager
例子裡,我們需要把事件的時間交給原生方法。我們不能在橋接通道里傳遞Date物件,所以需要把日期轉化成字串或數字來傳遞。我們可以這麼實現原生函式:
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];
}
不過我們可以依靠自動型別轉換的特性,跳過手動的型別轉換,而直接這麼寫:
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()); // 把日期以unix時間戳形式傳遞
也可以這樣:
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.toISOString()); // 把日期以ISO-8601的字串形式傳遞
兩個值都會被轉換為正確的NSDate
型別。但如果提供一個不合法的值,譬如一個Array
,則會產生一個“紅屏”報錯資訊。
隨著CalendarManager.addEvent
方法變得越來越複雜,引數的個數越來越多,其中有一些可能是可選的引數。在這種情況下我們應該考慮修改我們的API,用一個dictionary來存放所有的事件引數,像這樣:
#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裡這樣呼叫:
CalendarManager.addEvent('Birthday Party', {
location: '4 Privet Drive, Surrey',
time: date.toTime(),
description: '...'
})
注意: 關於陣列和對映
Objective-C並沒有提供確保這些結構體內部值的型別的方式。你的原生模組可能希望收到一個字串陣列,但如果JavaScript在呼叫的時候提供了一個混合number和string的陣列,你會收到一個
NSArray
,裡面既有NSNumber
也有NSString
。對於陣列來說,RCTConvert
提供了一些型別化的集合,譬如NSStringArray
或者UIColorArray
,你可以用在你的函式宣告中。對於對映而言,開發者有責任自己呼叫RCTConvert
的輔助方法來檢測和轉換值的型別。
回撥函式
警告
本章節內容目前還處在實驗階段,因為我們還並沒有太多的實踐經驗來處理回撥函式。
原生模組還支援一種特殊的引數——回撥函式。它提供了一個函式來把返回值傳回給JavaScript。
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]);
}
RCTResponseSenderBlock
只接受一個引數——傳遞給JavaScript回撥函式的引數陣列。在上面這個例子裡我們用Node.js的常用習慣:第一個引數是一個錯誤物件(沒有發生錯誤的時候為null),而剩下的部分是函式的返回值。
CalendarManager.findEvents((error, events) => {
if (error) {
console.error(error);
} else {
this.setState({events: events});
}
})
原生模組通常只應呼叫回撥函式一次。但是,它可以儲存callback並在將來呼叫。這在封裝那些通過“委託函式”來獲得返回值的iOS API時最為常見。RCTAlertManager
中就屬於這種情況。
如果你想傳遞一個更接近Error
型別的物件給Javascript,可以用RCTUtils.h
提供的RCTMakeError
函式。現在它僅僅是傳送了一個和Error結構一樣的dictionary給Javascript,但我們考慮在將來版本里讓它產生一個真正的Error
物件。
Promises
譯註:這一部分涉及到較新的js語法和特性,不熟悉的讀者建議先閱讀ES6的相關書籍和文件。
原生模組還可以使用promise來簡化程式碼,搭配ES2016(ES7)標準的async/await
語法則效果更佳。如果橋接原生方法的最後兩個引數是RCTPromiseResolveBlock
和RCTPromiseRejectBlock
,則對應的JS方法就會返回一個Promise物件。
我們把上面的程式碼用promise來代替回撥進行重構:
RCT_REMAP_METHOD(findEvents,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject))
{
NSArray *events = ...
if (events) {
resolve(events);
} else {
reject(error);
}
}
現在JavaScript端的方法會返回一個Promise。這樣你就可以在一個聲明瞭async
的非同步函式內使用await
關鍵字來呼叫,並等待其結果返回。(雖然這樣寫著看起來像同步操作,但實際仍然是非同步的,並不會阻塞執行來等待)。
async function updateEvents() {
try {
var events = await CalendarManager.findEvents();
this.setState({ events });
} catch (e) {
console.error(e);
}
}
updateEvents();
多執行緒
原生模組不應對自己被呼叫時所處的執行緒做任何假設。React Native在一個獨立的序列GCD佇列中呼叫原生模組的方法,但這屬於實現的細節,並且可能會在將來的版本中改變。通過實現方法- (dispatch_queue_t)methodQueue
,原生模組可以指定自己想在哪個佇列中被執行。具體來說,如果模組需要呼叫一些必須在主執行緒才能使用的API,那應當這樣指定:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
類似的,如果一個操作需要花費很長時間,原生模組不應該阻塞住,而是應當宣告一個用於執行操作的獨立佇列。舉個例子,RCTAsyncLocalStorage
模組建立了自己的一個queue,這樣它在做一些較慢的磁碟操作的時候就不會阻塞住React本身的訊息佇列:
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
指定的methodQueue
會被你模組裡的所有方法共享。如果你的方法中“只有一個”是耗時較長的(或者是由於某種原因必須在不同的佇列中執行的),你可以在函式體內用dispatch_async
方法來在另一個佇列執行,而不影響其他方法:
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在這裡執行長時間的操作
...
// 你可以在任何執行緒/佇列中執行回撥函式
callback(@[...]);
});
}
注意: 在模組之間共享分發佇列
methodQueue
方法會在模組被初始化的時候被執行一次,然後會被React Native的橋接機制儲存下來,所以你不需要自己儲存佇列的引用,除非你希望在模組的其它地方使用它。但是,如果你希望在若干個模組中共享同一個佇列,則需要自己儲存並返回相同的佇列例項;僅僅是返回相同名字的佇列是不行的。
匯出常量
原生模組可以匯出一些常量,這些常量在JavaScript端隨時都可以訪問。用這種方法來傳遞一些靜態資料,可以避免通過bridge進行一次來回互動。
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
Javascript端可以隨時同步地訪問這個資料:
console.log(CalendarManager.firstDayOfTheWeek);
但是注意這個常量僅僅在初始化的時候匯出了一次,所以即使你在執行期間改變constantToExport
返回的值,也不會影響到JavaScript環境下所得到的結果。
列舉常量
用NS_ENUM
定義的列舉型別必須要先擴充套件對應的RCTConvert方法才可以作為函式引數傳遞。
假設我們要匯出如下的NS_ENUM
定義:
typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
UIStatusBarAnimationNone,
UIStatusBarAnimationFade,
UIStatusBarAnimationSlide,
};
你需要這樣來擴充套件RCTConvert類:
@implementation RCTConvert (StatusBarAnimation)
RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)},
UIStatusBarAnimationNone, integerValue)
@end
接著你可以這樣定義方法並且匯出enum值作為常量:
- (NSDictionary *)constantsToExport
{
return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) }
};
RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
completion:(RCTResponseSenderBlock)callback)
你的列舉現在會用上面提供的選擇器進行轉換(上面的例子中是integerValue
),然後再傳遞給你匯出的函式。
給Javascript傳送事件
即使沒有被JavaScript呼叫,本地模組也可以給JavaScript傳送事件通知。最直接的方式是使用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)
);
...
// 千萬不要忘記忘記取消訂閱, 通常在componentWillUnmount函式中實現。
subscription.remove();
從Swift匯出
Swift不支援巨集,所以從Swift向React Native匯出類和函式需要多做一些設定,但是大致與Objective-C是相同的。
假設我們已經有了一個一樣的CalendarManager
,不過是用Swift實現的類:
// CalendarManager.swift
@objc(CalendarManager)
class CalendarManager: NSObject {
@objc func addEvent(name: String, location: String, date: NSNumber) -> Void {
// Date is ready to use!
}
}
注意: 你必須使用@objc標記來確保類和函式對Objective-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
請注意,一旦你在IOS中混用2種語言,
你還需要一個額外的橋接標頭檔案,稱作“bridging header”,用來匯出Objective-C檔案給Swift。如果你是通過Xcode選單中的File>New File
來建立的Swift檔案,Xcode會自動為你建立這個標頭檔案。在這個標頭檔案中,你需要引入RCTBridgeModule.h
。
// CalendarManager-Bridging-Header.h
#import "RCTBridgeModule.h"
同樣的,你也可以使用RCT_EXTERN_REMAP_MODULE
和RCT_EXTERN_REMAP_METHOD
來改變匯出模組和方法的JavaScript呼叫名稱。
瞭解更多資訊,請參閱RCTBridgeModule
.