【React Native】文件翻譯閱讀紀錄 - 指南(iOS) - 本機模組

by - 上午9:00

Facebook Open Source React Native

本機模組Native Modules

有時,應用程序需要訪問平台API,而React Native還沒有相應的模塊。也許你想重用一些現有的Objective-C,Swift或C ++代碼而不必在JavaScript中重新實現它,或者編寫一些高性能的多線程代碼,例如用於圖像處理,數據庫或任何數量的高級擴展。

我們設計了React Native,使您可以編寫真正的本機代碼並可以訪問平台的全部功能。這是一個更高級的功能,我們不希望它成為通常開發過程的一部分,但它必須存在。如果React Native不支持您需要的本機功能,您應該能夠自己構建它。

這是一個更高級的指南,展示瞭如何構建本機模塊。它假設讀者了解Objective-C或Swift和核心庫(Foundation,UIKit)。

iOS日曆模塊示例


本指南將使用iOS Calendar API示例。假設我們希望能夠從JavaScript訪問iOS日曆。

本機模塊只是一個實現RCTBridgeModule協議的Objective-C類。如果您想知道,RCT是ReaCT的縮寫。
// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end
除了實現 RCTBridgeModule 協議之外,您的類還必須包含 RCT_EXPORT_MODULE()。這需要一個可選參數,該參數指定模塊可以在JavaScript 代碼中訪問的名稱(稍後將詳細介紹)。如果未指定名稱,則 JavaScript 模塊名稱將與 Objective-C 類名稱匹配。如果 Objective-C 類名稱以 RCT 開頭,則JavaScript模塊名稱將排除RCT前綴。
// CalendarManager.m
@implementation CalendarManager

// To export a module named CalendarManager
RCT_EXPORT_MODULE();

// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager);

@end
除非明確告知,否則 React Native 不會將 CalendarManager 的任何方法暴露給JavaScript。這是使用 RCT_EXPORT_METHOD() 完成的:
#import "CalendarManager.h"
#import <React/RCTLog.h>

@implementation CalendarManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
現在,您可以從JavaScript文件中調用如下方法:
import {NativeModules} from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');
注意:JavaScript方法名稱

導出到JavaScript的方法的名稱是本機方法的名稱,直到第一個冒號。 React Native還定義了一個名為RCT_REMAP_METHOD()的宏來指定JavaScript方法的名稱。當多個本機方法在第一個冒號之前相同並且具有衝突的JavaScript名稱時,這很有用。
使用[CalendarManager new]調用在Objective-C端實例化CalendarManager模塊。橋接方法的返回類型始終無效。 React Native橋是異步的,因此將結果傳遞給JavaScript的唯一方法是使用回調或發出事件(見下文)。

參數類型

RCT_EXPORT_METHOD 支持所有標準 JSON 對像類型,例如:
  • string (NSString)
  • number (NSIntegerfloatdoubleCGFloatNSNumber)
  • boolean (BOOLNSNumber)
  • array (NSArray) 此列表中的任何類型的數組
  • object (NSDictionary) 包含此列表中的字符串鍵和任何類型的值
  • function (RCTResponseSenderBlock)
但它也適用於RCTConvert類支持的任何類型(有關詳細信息,請參閱RCTConvert)。 RCTConvert幫助函數都接受JSON值作為輸入,並將其映射到本機Objective-C類型或類。

在我們的CalendarManager示例中,我們需要將事件日期傳遞給本機方法。我們無法通過網橋發送JavaScript Date對象,因此我們需要將日期轉換為字符串或數字。我們可以像這樣編寫我們的本機函數:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull 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()
); // passing date as number of milliseconds since Unix epoch
or
CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.toISOString()
); // passing date as ISO-8601 string
並且這兩個值都將正確轉換為本機NSDate。一個錯誤的值,如數組,將生成一個有用的“RedBox”錯誤消息。

隨著CalendarManager.addEvent方法變得越來越複雜,參數的數量將會增加。其中一些可能是可選的。在這種情況下,值得考慮更改API以接受事件屬性的字典,如下所示:
#import <React/RCTConvert.h>

RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
  NSString *location = [RCTConvert NSString:details[@"location"]];
  NSDate *time = [RCTConvert NSDate:details[@"time"]];
  ...
}
並從JavaScript調用它:
CalendarManager.addEvent('Birthday Party', {
  location: '4 Privet Drive, Surrey',
  time: date.getTime(),
  description: '...',
});
注意:關於數組和映射

Objective-C不對這些結構中的值類型提供任何保證。您的本機模塊可能需要一個字符串數組,但如果JavaScript使用包含數字和字符串的數組調用您的方法,您將獲得包含NSNumber和NSString混合的NSArray。對於數組,RCTConvert提供了一些可以在方法聲明中使用的類型集合,例如NSStringArray或UIColorArray。對於地圖,開發人員有責任通過手動調用RCTConvert幫助程序方法單獨檢查值類型。

回調

警告

這部分比其他部分更具實驗性,因為我們還沒有圍繞回調的一套可靠的最佳實踐。
本機模塊還支持一種特殊的參數 - 回調。在大多數情況下,它用於向JavaScript提供函數調用結果。
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
  NSArray *events = ...
  callback(@[[NSNull null], events]);
}
RCTResponseSenderBlock 只接受一個參數 - 一個傳遞給JavaScript回調的參數數組。在這種情況下,我們使用Node的約定使第一個參數成為一個錯誤對象(當沒有錯誤時通常為null),其餘的是函數的結果。
CalendarManager.findEvents((error, events) => {
  if (error) {
    console.error(error);
  } else {
    this.setState({events: events});
  }
});
本機模塊應該只調用一次回調。可以存儲回調並稍後調用它。此模式通常用於包裝需要委託的iOS API - 有關示例,請參閱RCTAlertManager。如果從不調用回調,則會洩漏一些內存。如果同時傳遞onSuccess和onFail回調,則只應調用其中一個。

如果要將類似錯誤的對像傳遞給JavaScript,請使用RCTUtils.h中的RCTMakeError。現在,這只是將一個錯誤形狀的字典傳遞給JavaScript,但我們希望將來自動生成真正的JavaScript Error對象。

Promises(承諾)

本機模塊也可以履行承諾,這可以簡化您的代碼,尤其是在使用ES2016的async / await語法時。當橋接本機方法的最後一個參數是RCTPromiseResolveBlock和RCTPromiseRejectBlock時,其相應的JS方法將返回一個JS Promise對象。

重構上面的代碼以使用promise而不是回調看起來像這樣:
RCT_REMAP_METHOD(findEvents,
                 findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSArray *events = ...
  if (events) {
    resolve(events);
  } else {
    NSError *error = ...
    reject(@"no_events", @"There were no events", error);
  }
}
此方法的JavaScript副本返回Promise。這意味著您可以在異步函數中使用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方法允許本機模塊指定其方法應在哪個隊列上運行。例如,如果它需要使用僅主線程的iOS API,則應通過以下方式指定:
- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}
同樣,如果某個操作可能需要很長時間才能完成,則本機模塊不應該阻塞,並且可以指定它自己的隊列來運行操作。例如,RCTAsyncLocalStorage模塊創建自己的隊列,因此不會阻止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), ^{
    // Call long-running code on background thread
    ...
    // You can invoke callback from any thread/queue
    callback(@[...]);
  });
}
注意:在模塊之間共享調度隊列

初始化模塊時,methodQueue方法將被調用一次,然後由橋保留,因此不需要自己保留隊列,除非您希望在模塊中使用它。但是,如果您希望在多個模塊之間共享相同的隊列,那麼您需要確保為每個模塊保留並返回相同的隊列實例;只返回一個相同名稱的隊列將無法正常工作。

依賴注入

橋自動初始化任何已註冊的RCTBridgeModules,但是您可能希望實例化您自己的模塊實例(例如,您可以注入依賴項)。

您可以通過創建一個實現RCTBridgeDelegate協議的類,使用委託作為參數初始化RCTBridge並使用初始化橋初始化RCTRootView來完成此操作。
id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];

RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];

RCTRootView *rootView = [[RCTRootView alloc]
                        initWithBridge:bridge
                            moduleName:kModuleName
                     initialProperties:nil];

導出常量

導出常量
- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}
JavaScript可以立即使用此值,同步:
console.log(CalendarManager.firstDayOfTheWeek);
請注意,常量僅在初始化時導出,因此如果在運行時更改 constantsToExport 值,則不會影響 JavaScript 環境。

枚舉常量

在沒有首先擴展RCTConvert的情況下,通過NS_ENUM定義的枚舉不能用作方法參數。

要導出以下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
然後,您可以定義方法並導出枚舉常量,如下所示:
- (NSDictionary *)constantsToExport
{
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
};

RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)
然後,在傳遞給導出的方法之前,將使用提供的選擇器(上例中的integerValue)自動解包您的枚舉。

將事件發送到 JavaScript

本機模塊可以向JavaScript發送事件,而無需直接調用。執行此操作的首選方法是子類化 RCTEventEmitter,實現 supportedEvents 並調用 self sendEventWithName:
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>

@end
// CalendarManager.m
#import "CalendarManager.h"

@implementation CalendarManager

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents
{
  return @[@"EventReminder"];
}

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
}

@end
JavaScript代碼可以通過在模塊周圍創建新的NativeEventEmitter實例來訂閱這些事件。
import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;

const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);

const subscription = calendarManagerEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...
// Don't forget to unsubscribe, typically in componentWillUnmount
subscription.remove();
有關將事件發送到JavaScript的更多示例,請參閱 RCTLocationObserver.

優化監聽

如果在沒有聽眾的情況下通過發出事件而不必要地花費資源,您將收到警告。要避免這種情況,並優化模塊的工作負載(例如,通過取消訂閱上游通知或暫停後台任務),您可以覆蓋 RCTEventEmitter 子類中的 startObserving 和 stopObserving。
@implementation CalendarManager
{
  bool hasListeners;
}

// Will be called when this module's first listener is added.
-(void)startObserving {
    hasListeners = YES;
    // Set up any upstream listeners or background tasks as necessary
}

// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving {
    hasListeners = NO;
    // Remove upstream listeners, stop unnecessary background tasks
}

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  if (hasListeners) { // Only send events if anyone is listening
    [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
  }
}

導出 Swift

Swift不支持宏,因此將它暴露給React Native需要更多的設置,但工作方式相同。

假設我們有相同的CalendarManager但是作為Swift類:
// CalendarManager.swift

@objc(CalendarManager)
class CalendarManager: NSObject {

  @objc(addEvent:location:date:)
  func addEvent(name: String, location: String, date: NSNumber) -> Void {
    // Date is ready to use!
  }

  @objc
  func constantsToExport() -> [String: Any]! {
    return ["someKey": "someValue"]
  }

}
注意:使用@objc修飾符確保將類和函數正確導出到Objective-C運行時非常重要。
然後創建一個私有實現文件,該文件將使用React Native網橋註冊所需信息:
// CalendarManagerBridge.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)

RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)

@end
對於剛接觸 Swift 和 Objective-C 的人來說,每當你在 iOS 項目中混合使用這兩種語言時,你還需要一個額外的橋接文件,稱為橋接頭,將 Objective-C文件公開給Swift。如果您通過 Xcode 文件>新建文件菜單選項將 Swift 文件添加到您的應用程序,Xcode 將為您創建此頭文件。您需要在此頭文件中導入 RCTBridgeModule.h。
// CalendarManager-Bridging-Header.h
#import <React/RCTBridgeModule.h> 
您還可以使用 RCT_EXTERN_REMAP_MODULE 和_RCT_EXTERN_REMAP_METHOD 來更改要導出的模塊或方法的 JavaScript 名稱。有關更多信息,請參閱 RCTBridgeModule.
在製作第三方模塊時很重要:只有Xcode 9及更高版本支持使用Swift的靜態庫。為了在您在模塊中包含的iOS靜態庫中使用Swift時構建Xcode項目,您的主應用程序項目必須包含Swift代碼和橋接頭本身。如果您的應用程序項目不包含任何Swift代碼,則解決方法可以是單個空的.swift文件和空橋接頭。



You May Also Like

0 意見