Native 與 React Native 之間的溝通
在與現有應用程式整合指南和Native UI 組件指南中,我們學習了如何將 React Native 嵌入到 Native 組件中,反之亦然。當我們混合使用 Native 和 React Native 組件時,最終會發現需要在這兩個世界之間進行溝通。其他指南已經提到了一些實現此目標的方法。本文總結了可用的技術。
簡介
React Native 的靈感來自 React,因此資訊流的基本概念是相似的。React 中的流程是單向的。我們維護組件的層次結構,其中每個組件僅依賴於其父組件及其自身的內部狀態。我們使用屬性來做到這一點:資料以自上而下的方式從父組件傳遞到其子組件。如果祖先組件依賴於其後代組件的狀態,則應向下傳遞一個回呼,供後代組件用於更新祖先組件。
相同的概念適用於 React Native。只要我們完全在框架內建構應用程式,我們就可以使用屬性和回呼來驅動我們的應用程式。但是,當我們混合使用 React Native 和 Native 組件時,我們需要一些特定的跨語言機制,以便我們在它們之間傳遞資訊。
屬性
屬性是跨組件溝通最直接的方式。因此,我們需要一種方法來將屬性從 Native 傳遞到 React Native,以及從 React Native 傳遞到 Native。
從 Native 傳遞屬性到 React Native
為了將 React Native 視圖嵌入到 Native 組件中,我們使用 RCTRootView
。RCTRootView
是一個 UIView
,它持有 React Native 應用程式。它還提供了 Native 端和託管應用程式之間的介面。
RCTRootView
有一個初始化器,允許您將任意屬性向下傳遞到 React Native 應用程式。initialProperties
參數必須是 NSDictionary
的實例。該字典在內部轉換為 JSON 物件,頂層 JS 組件可以引用它。
NSArray *imageList = @[@"https://dummyimage.com/600x400/ffffff/000000.png",
@"https://dummyimage.com/600x400/000000/ffffff.png"];
NSDictionary *props = @{@"images" : imageList};
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
import React from 'react';
import {View, Image} from 'react-native';
export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return <Image source={{uri: imgURI}} />;
}
render() {
return <View>{this.props.images.map(this.renderImage)}</View>;
}
}
RCTRootView
還提供了一個讀寫屬性 appProperties
。設定 appProperties
後,React Native 應用程式會使用新的屬性重新渲染。只有當新的更新屬性與之前的屬性不同時,才會執行更新。
NSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];
rootView.appProperties = @{@"images" : imageList};
隨時更新屬性都沒問題。但是,更新必須在主執行緒上執行。您可以在任何執行緒上使用 getter。
目前,有一個已知的問題,在橋接啟動期間設定 appProperties,變更可能會遺失。請參閱 https://github.com/facebook/react-native/issues/20115 以獲取更多資訊。
沒有辦法一次僅更新幾個屬性。我們建議您將其建置到您自己的包裝器中。
從 React Native 傳遞屬性到 Native
公開 Native 組件屬性的問題在本文中詳細介紹。簡而言之,在您的自訂 Native 組件中使用 RCT_CUSTOM_VIEW_PROPERTY
巨集匯出屬性,然後在 React Native 中使用它們,就好像該組件是一個普通的 React Native 組件一樣。
屬性的限制
跨語言屬性的主要缺點是它們不支援回呼,這將允許我們處理由下而上的資料繫結。想像一下,您有一個小的 RN 視圖,您希望由於 JS 操作而從 Native 父視圖中移除它。使用 props 無法做到這一點,因為資訊需要由下而上傳遞。
儘管我們有跨語言回呼的一種形式(在此處描述),但這些回呼並不總是我們需要的東西。主要問題是它們不打算作為屬性傳遞。相反,這種機制允許我們從 JS 觸發 Native 操作,並在 JS 中處理該操作的結果。
跨語言互動的其他方式(事件和 Native 模組)
如上一章所述,使用屬性有一些限制。有時屬性不足以驅動我們應用程式的邏輯,我們需要一個提供更多彈性的解決方案。本章介紹 React Native 中可用的其他溝通技術。它們可用於內部溝通(RN 中 JS 和 Native 層之間)以及外部溝通(RN 和應用程式的「純 Native」部分之間)。
React Native 使您能夠執行跨語言的函式呼叫。您可以從 JS 執行自訂 Native 程式碼,反之亦然。不幸的是,根據我們正在處理的方面,我們以不同的方式實現相同的目標。對於 Native - 我們使用事件機制來排程在 JS 中執行處理函式,而對於 React Native,我們直接呼叫 Native 模組匯出的方法。
從 Native 呼叫 React Native 函式(事件)
事件在本文中詳細介紹。請注意,使用事件不能保證執行時間,因為事件是在單獨的執行緒上處理的。
事件功能強大,因為它們允許我們變更 React Native 組件,而無需引用它們。但是,在使用它們時,您可能會遇到一些陷阱
- 由於事件可以從任何地方傳送,因此它們可能會將義大利麵條式的依賴關係引入您的專案。
- 事件共用命名空間,這意味著您可能會遇到一些名稱衝突。衝突不會被靜態偵測到,這使得它們難以除錯。
- 如果您使用相同 React Native 組件的多個實例,並且您想從事件的角度區分它們,您可能需要引入識別碼並將它們與事件一起傳遞(您可以使用 Native 視圖的
reactTag
作為識別碼)。
當我們將 Native 嵌入到 React Native 中時,我們使用的常見模式是使 Native 組件的 RCTViewManager 成為視圖的委派,透過橋接將事件傳送回 JavaScript。這將相關的事件呼叫保持在一個位置。
從 React Native 呼叫 Native 函式(Native 模組)
Native 模組是 Objective-C 類別,可在 JS 中使用。通常,每個 JS 橋接器會建立每個模組的一個實例。它們可以將任意函式和常數匯出到 React Native。它們在本文中已詳細介紹。
Native 模組是單例模式的事實限制了嵌入環境中的機制。假設我們有一個嵌入在 Native 視圖中的 React Native 組件,並且我們想要更新 Native 父視圖。使用 Native 模組機制,我們將匯出一個函式,該函式不僅接受預期的引數,還接受父 Native 視圖的識別碼。該識別碼將用於檢索對父視圖的引用以進行更新。也就是說,我們需要在模組中保留從識別碼到 Native 視圖的映射。
儘管此解決方案很複雜,但它在 RCTUIManager
中使用,RCTUIManager
是一個內部 React Native 類別,用於管理所有 React Native 視圖。
Native 模組也可用於將現有的 Native 函式庫公開給 JS。Geolocation 函式庫是這個想法的一個生動範例。
所有 Native 模組共用相同的命名空間。建立新的模組時,請注意名稱衝突。
版面配置計算流程
當整合 Native 和 React Native 時,我們還需要一種方法來整合兩個不同的版面配置系統。本節介紹常見的版面配置問題,並簡要描述解決這些問題的機制。
嵌入在 React Native 中的 Native 組件的版面配置
這種情況在本文中介紹。總而言之,由於我們所有的 Native React 視圖都是 UIView
的子類別,因此大多數樣式和大小屬性都將像您預期的那樣開箱即用。
嵌入在 Native 中的 React Native 組件的版面配置
具有固定大小的 React Native 內容
一般情況是我們有一個具有固定大小的 React Native 應用程式,Native 端知道該大小。特別是,全螢幕 React Native 視圖屬於這種情況。如果我們想要一個更小的根視圖,我們可以顯式設定 RCTRootView 的 frame。
例如,要使 RN 應用程式的高度為 200(邏輯)像素,並且託管視圖的寬度很寬,我們可以執行以下操作
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGRectMake(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}
當我們有一個固定大小的根視圖時,我們需要在 JS 端尊重其邊界。換句話說,我們需要確保 React Native 內容可以包含在固定大小的根視圖中。確保這一點的最簡單方法是使用 Flexbox 版面配置。如果您使用絕對定位,並且 React 組件在根視圖的邊界外可見,您將與 Native 視圖重疊,導致某些功能行為異常。例如,「TouchableHighlight」不會突出顯示您在根視圖邊界外的觸摸。
透過重新設定其 frame 屬性來動態更新根視圖的大小是完全可以的。React Native 將負責內容的版面配置。
具有彈性大小的 React Native 內容
在某些情況下,我們希望渲染初始大小未知的內容。假設大小將在 JS 中動態定義。我們有兩種解決方案來解決這個問題。
- 您可以將 React Native 視圖包裝在
ScrollView
組件中。這保證了您的內容始終可用,並且不會與 Native 視圖重疊。 - React Native 允許您在 JS 中確定 RN 應用程式的大小,並將其提供給託管
RCTRootView
的所有者。然後,所有者負責重新佈局子視圖並保持 UI 一致。我們使用RCTRootView
的彈性模式來實現這一點。
RCTRootView
支援 4 種不同的尺寸彈性模式
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};
RCTRootViewSizeFlexibilityNone
是預設值,它使根視圖的大小固定(但仍然可以使用 setFrame:
更新)。其他三種模式允許我們追蹤 React Native 內容的大小更新。例如,將模式設定為 RCTRootViewSizeFlexibilityHeight
將導致 React Native 測量內容的高度,並將該資訊傳遞回 RCTRootView
的委派。可以在委派中執行任意操作,包括設定根視圖的 frame,以便內容適合。僅當內容的大小發生變更時,才會呼叫委派。
在 JS 和 Native 中都使尺寸具有彈性會導致未定義的行為。例如 - 當您在託管 RCTRootView
上使用 RCTRootViewSizeFlexibilityWidth
時,不要使頂層 React 組件的寬度具有彈性(使用 flexbox
)。
讓我們看一個範例。
- (instancetype)initWithFrame:(CGRect)frame
{
[...]
_rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"FlexibilityExampleApp"
initialProperties:@{}];
_rootView.delegate = self;
_rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
_rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}
#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
CGRect newFrame = rootView.frame;
newFrame.size = rootView.intrinsicContentSize;
rootView.frame = newFrame;
}
在範例中,我們有一個 FlexibleSizeExampleView
視圖,它持有一個根視圖。我們建立根視圖,初始化它並設定委派。委派將處理大小更新。然後,我們將根視圖的尺寸彈性設定為 RCTRootViewSizeFlexibilityHeight
,這意味著每次 React Native 內容變更其高度時,都會呼叫 rootViewDidChangeIntrinsicSize:
方法。最後,我們設定根視圖的寬度和位置。請注意,我們在那裡也設定了高度,但它沒有效果,因為我們使高度依賴於 RN。
您可以在此處查看範例的完整原始碼。
動態變更根視圖的尺寸彈性模式是沒問題的。變更根視圖的彈性模式將排程版面配置重新計算,並且一旦內容大小已知,將呼叫委派 rootViewDidChangeIntrinsicSize:
方法。
React Native 版面配置計算在單獨的執行緒上執行,而 Native UI 視圖更新在主執行緒上完成。這可能會導致 Native 和 React Native 之間出現暫時的 UI 不一致。這是一個已知的問題,我們的團隊正在努力同步來自不同來源的 UI 更新。
在根視圖成為某些其他視圖的子視圖之前,React Native 不會執行任何版面配置計算。如果您想在 React Native 視圖的尺寸已知之前隱藏它,請將根視圖新增為子視圖,並使其最初處於隱藏狀態(使用 UIView
的 hidden
屬性)。然後在委派方法中變更其可見性。