終於開始新一篇的填坑之旅了。RN厲害的一個地方就是RN可以和Native元件通訊。這個Native元件包括native的庫和自定義檢視,我們今天主要設計的內容是native庫方面的只是。自定義檢視的使用會在後面講到。
坑是什麼樣的坑
主要的是遇到一個業務需求,需要檢測當前應用的版本是什麼。需要返回當前的版本號和build數。
主要的需求在native來說非常簡單:
NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
NSString * build = [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey];
兩句分別獲得了版本號和build數。
開始填坑
填坑其實也是意外的簡單。當然,我們不準備把這個程式碼作為庫釋出到npm上給別人用,所以複雜度自然降低了不少。
首先、在Xcode裡建立RNUpgrade
類作為後面和RN通訊的native元件。這會在專案裡建立兩個objc的檔案RNUpgrade.h和RNUpgrade.m。
在RNUpgrade.h標頭檔案中,新增RCTBridgeModule
協議。要給RN暴露介面這個協議是必須的。
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
@interface RNUpgrade : NSObject<RCTBridgeModule>
@end
之後對於標頭檔案就可以什麼都不用管了。至少對於暴露介面這件事是這樣的。
下面就來看原始檔吧。
看文件,要暴露native方法就必須在原始檔裡包含一個巨集的呼叫,這個巨集是:RCT_EXPORT_MODULE()
。這個巨集可以包含一個引數指定RN中訪問這個模組的名字。預設的就是你的objc類的名字。
#import "RNUpgrade.h"
#import "RCTUtils.h"
#import "AppDelegate.h"
NSString *const RNUPGRADE_ERROR_DOMAIN = @"Upgrade info error";
@implementation RNUpgrade
RCT_EXPORT_MODULE();
@end
那麼如何來暴露出一個方法呢?使用RCT_EXPORT_METHOD()
巨集。官網的例子:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
巨集RCT_EXPORT_METHOD
的引數就是這個方法的宣告部分,方法體在外面。RCT_EXPORT_METHOD(someMethod:(NSString*)stringParameter)
這樣的,然後外面寫方法體。
那麼,我要返回現在APP的版本資訊就可以寫成這樣:
RCT_EXPORT_METHOD(getCurrentInfo) {
@try {
NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
NSString * build = [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey];
return @{@"versionName": version, @"versionCode": build}
} @catch (NSException *exception) {
//Log error info...
}
}
但是,如何返回字典呢?直接return?接著差文件。
暴露給RN的方法是不能直接返回任何東西的。因為RN的呼叫時非同步的,所以只能使用回撥的方式,或者觸發事件的方式實現返回值。
回撥!看個官網的例子:
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]);
}
好的,回撥就說到這裡了。因為筆者的專案已經上了async/await
了,回撥就顯得沒啥必要了。而且,文件顯示。RN也提供了暴露介面返回Promise
的支援。只需要在方法裡接受兩個引數,一個resolver
,一個rejecter
:
RCT_EXPORT_METHOD(getCurrentInfo:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
@try {
NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
NSString * build = [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey];
resolve(@{@"versionName": version, @"versionCode": build});
} @catch (NSException *exception) {
NSError *error = [NSError errorWithDomain:RNUPGRADE_ERROR_DOMAIN code:1 userInfo: exception.userInfo];
reject(exception.name, exception.reason, error);
}
}
於是,這樣就可以返回一個Promise
了。
在RN的專案裡呼叫這個方法:
// 首先通過`NativeModules`接收暴露的native模組。
import { NativeModules } from "react-native"
const upgrade = NativeModules.RNUpgrade
// 方法呼叫
const ret = await Promise.all([upgrade.getCurrentInfo(), upgrade.getUpgradeInfo()])
沒錯,模組還有另外一個native方法。這個native方法也返回一個Promise
。
返回宣告相同的native方法
其實在native模組裡很多方法的宣告都是一模一樣的:resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject
。因為我只需要接收一個resolver
和rejecter
以便返回一個Promise
。於是就用到了RN提供的另外一個巨集:RCT_REMAP_METHOD
。這個巨集專門用來處理宣告基本一樣的情況。它會把native裡的宣告基本一樣的巨集對映到一個唯一的RN方法名稱上。
RCT_REMAP_METHOD(getCurrentInfo,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
@try {
NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
NSString * build = [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey];
resolve(@{@"versionName": version, @"versionCode": build});
} @catch (NSException *exception) {
NSError *error = [NSError errorWithDomain:RNUPGRADE_ERROR_DOMAIN code:1 userInfo: exception.userInfo];
reject(exception.name, exception.reason, error);
}
}
基本上在專案裡如何暴露一個native方法給RN的js呼叫非常簡單,就如上面所述一樣。
- 在標頭檔案裡繼承了
RCTBridgeModule
協議。 - 在原始檔裡使用
RCT_EXPORT_MODULE();
巨集。 - 使用巨集
RCT_EXPORT_METHOD
暴露方法。
如果方法需要返回值的話使用回撥、或者Promise。這也只是native方法寫幾個引數的問題。
重要的一點:執行緒
在文件中有這麼一點:多執行緒。千萬不要根據RN實現的一些細節就假設你的模組執行在某某執行緒上。官網也說了,這個是會變的。如果你要確定你的程式碼執行在什麼執行緒上,通過方法- (dispatch_queue_t)methodQueue
來指定。
注意:指定的methodQueue會被你模組裡的所有方法共享。
如果執行在主執行緒上:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
如果執行在自己建立的執行緒上:
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
如果模組裡只有小部分程式碼執行在其他的執行緒上,可以使用native裡傳統的方法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的橋接機制儲存下來,所以你不需要自己儲存佇列的引用。
省心省力!