在原生和React Native間通訊
通過植入原生應用和原生UI元件兩篇文件,我們學習了React Native和原生元件的互相整合。在整合的過程中,我們會需要在兩個世界間互相通訊。有些方法已經在其他的指南中提到了,這篇文章總結了所有可行的技術。
簡介
React Native是從React中得到的靈感,因此基本的資訊流是類似的。在React中資訊是單向的。我們維護了元件層次,在其中每個元件都僅依賴於它父母和自己的狀態。通過屬性(properties)我們將資訊從上而下的從父母傳遞到子元素。如果一個祖先元件需要自己子孫的狀態,推薦的方法是傳遞一個回撥函式給對應的子元素。
React Native也運用了相同的概念。只要我們完全在框架內構建應用,就可以通過屬性和回撥函式來調動整個應用。但是,當我們混合React Native和原生元件時,我們需要一些特殊的,跨語言的機制來傳遞資訊。
屬性
屬性是最簡單的跨元件通訊。因此我們需要一個方法從原生元件傳遞屬性到React Native或者從React Native到原生元件。
從原生元件傳遞屬性到React Native
我們使用RCTRootView
將React Natvie檢視封裝到原生元件中。RCTRootView
是一個UIView
容器,承載著React
Native應用。同時它也提供了一個聯通原生端和被託管端的介面。
通過RCTRootView
的初始化函式你可以將任意屬性傳遞給React Native應用。引數initialProperties
必須是NSDictionary
的一個例項。這一字典引數會在內部被轉化為一個可供JS元件呼叫的JSON物件。
NSArray *imageList = @[@"http://foo.com/bar1.png",
@"http://foo.com/bar2.png"];
NSDictionary *props = @{@"images" : imageList};
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
'use strict';
var React = require('react-native');
var {
View,
Image
} = React;
class ImageBrowserApp extends React.Component {
renderImage: function(imgURI) {
return (
<Image source={{uri: imgURI}} />
);
},
render() {
return (
<View>
{this.props.images.map(this.renderImage)}
</View>
);
}
}
React.AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);
RCTRootView
同樣提供了一個可讀寫的屬性appProperties
。在appProperties
設定之後,React
Native應用將會根據新的屬性重新渲染。當然,只有在新屬性和之前的屬性有區別時更新才會被觸發。
NSArray *imageList = @[@"http://foo.com/bar3.png",
@"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};
你可以隨時更新屬性,但是更新必須在主執行緒中進行,讀取則可以在任何執行緒中進行。
更新屬性時並不能做到只更新一部分屬性。我們建議你自己封裝一個函式來構造屬性。
注意:目前,最頂層的RN元件(即registerComponent方法中呼叫的那個)的
componentWillReceiveProps
和componentWillUpdateProps
方法在屬性更新後不會觸發。但是,你可以通過componentWillMount
訪問新的屬性值。
從React Native傳遞屬性到原生元件
這篇文件詳細討論了暴露原生元件屬性的問題。簡而言之,在你自定義的原生元件中通過RCT_CUSTOM_VIEW_PROPERTY
巨集匯出屬性,就可以直接在React
Native中使用,就好像它們是普通的React Native元件一樣。
屬性的限制
跨語言屬性的主要缺點是不支援回撥方法,因而無法實現自下而上的資料繫結。設想你有一個小的RN檢視,當一個JS動作觸發時你想從原生的父檢視中移除它。此時你會發現根本做不到,因為資訊需要自下而上進行傳遞。
雖然我們有跨語言回撥(參閱這裡,但是這些回撥函式並不總能滿足需求。最主要的問題是它們並不是被設計來當作屬性進行傳遞。這一機制的本意是允許我們從JS觸發一個原生動作,然後用JS處理那個動作的處理結果。
其他的跨語言互動(事件和原生模組)
如上一章所說,使用屬性總會有一些限制。有時候屬性並不足以滿足應用邏輯,因此我們需要更靈活的解決辦法。這一章描述了其他的在React Native中可用的通訊方法。他們可以用來內部通訊(在JS和RN的原生層之間),也可以用作外部通訊(在RN和純原生部分之間)。
React Native允許使用跨語言的函式呼叫。你可以在JS中呼叫原生程式碼,也可以在原生程式碼中呼叫JS。在不同端需要用不同的方法來實現相同的目的。在原生程式碼中我們使用事件機制來排程JS中的處理函式,而在React Native中我們直接使用原生模組匯出的方法。
從原生程式碼呼叫React Natvie函式(事件)
事件的詳細用法在這篇文章中進行了討論。注意使用事件無法確保執行的時間,因為事件的處理函式是在單獨的執行緒中執行。
事件很強大,它可以不需要引用直接修改React Native元件。但是,當你使用時要注意下面這些陷阱:
- 由於事件可以從各種地方產生,它們可能導致混亂的依賴。
- 事件共享相同的名稱空間,因此你可能遇到名字衝突。衝突不會在編寫程式碼時被探測到,因此很難排錯。
- 如果你使用了同一個React Native元件的多個引用,然後想在事件中區分它們,name你很可能需要在事件中同時傳遞一些標識(你可以使用原生檢視中的
reactTag
作為標識)。
在React Native中嵌入原生元件時,通常的做法是用原生元件的RCTViewManager作為檢視的代理,通過bridge向JS傳送事件。這樣可以集中在一處呼叫相關的事件。
從React Native中呼叫原生方法(原生模組)
原生模組是JS中也可以使用的Objective-C類。一般來說這樣的每一個模組的例項都是在每一次通過JS bridge通訊時建立的。他們可以匯出任意的函式和常量給React Native。相關細節可以參閱這篇文章。
事實上原生模組的單例項模式限制了嵌入。假設我們有一個React Native元件被嵌入了一個原生檢視,並且我們希望更新原生的父檢視。使用原生模組機制,我們可以匯出一個函式,不僅要接收預設引數,還要接收父檢視的標識。這個標識將會用來獲得父檢視的引用以更新父檢視。那樣的話,我們需要維持模組中標識到原生模組的對映。 雖然這個解決辦法很複雜,它仍被用在了管理所有React Native檢視的RCTUIManager
類中,
原生模組同樣可以暴露已有的原生庫給JS,地理定位庫就是一個現成的例子。
警告:所有原生模組共享同一個名稱空間。建立新模組時注意命名衝突。
佈局計算流
當整合原生模組和React Natvie時,我們同樣需要一個能協同不同的佈局系統的辦法。這一章節討論了常見的佈局問題,並且提供瞭解決機制的簡單說明。
在React Native中嵌入一個原生元件
這個情況在這篇文章中進行了討論。基本上,由於所有的原生檢視都是UIView
的子集,大多數型別和尺寸屬性將和你期望的一樣可以使用。
在原生中嵌入一個React Native元件
固定大小的React Native內容
最簡單的情況是一個對於原生端已知的,固定大小的React Native應用,尤其是一個全屏的React Native檢視。如果我們需要一個小一點的根檢視,我們可以明確的設定RCTRootView
的frame。
比如說,建立一個200畫素高,宿主檢視那樣寬的RN app,我們可以這樣做:
// SomeViewController.m
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGMakeRect(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}
當我們建立了一個固定大小的根檢視,則需要在JS中遵守它的邊界。換句話說,我們需要確保React Native內容能夠在固定的大小中放下。最簡單的辦法是使用flexbox佈局。如果你使用絕對定位,並且React元件在根檢視邊界外可見,則React Native元件將會和原生檢視重疊,導致某些不符合期望的行為。比如說,當你點選根檢視邊界之外的區域TouchableHighlight
將不會高亮。
通過重新設定frame的屬性來動態更新根檢視的大小是完全可行的。React Native將會關注內容佈局的變化。
彈性大小的React Native
有時候我們需要渲染一些不知道大小的內容。假設尺寸將會在JS中動態指定。我們有兩個解決辦法。
- 你可以將React Native檢視包裹在
ScrollView
中。這樣可以保證你的內容總是可以訪問,並且不會和原生檢視重疊。 - React Native允許你在JS中決定RN應用的尺寸,並且將它傳遞給宿主檢視
RCTRootView
。然後宿主檢視將重新佈局子檢視,保證UI統一。我們通過RCTRootView
的彈性模式來達到目的。
RCTRootView
支援4種不同的彈性模式:
// RCTRootView.h
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};
預設值是RCTRootViewSizeFlexibilityNone
,表示使用固定大小的根檢視(仍然可以通過setFrame
更改)。其他三種模式可以跟蹤React
Native尺寸的變化。比如說,設定模式為RCTRootViewSizeFlexibilityHeight
,React Native將會測量內容的高度然後傳遞迴RCTRootView
的代理。代理可以執行任意的行為,包括設定根檢視的frame以使內容尺寸相匹配。
代理僅僅在內容的尺寸發生變化時才進行呼叫。
注意:在JS和原生中都設定彈性尺寸可能導致不確定的行為。比如--不要在設定
RCTRootView
為RCTRootViewSizeFlexibilityWidth
時同時指定最頂層的RN元件寬度可變(使用Flexbox)。
看一個例子。
// FlexibleSizeExampleView.m
- (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.intrinsicSize;
rootView.frame = newFrame;
}
在例子中我們使用一個FlexibleSizeExampleView
檢視來包含根檢視。我們建立了根檢視,初始化並且設定了代理。代理將會處理尺寸更新。然後,我們設定根檢視的彈性尺寸為RCTRootViewSizeFlexibilityHeight
,意味著rootViewDidChangeIntrinsicSize:
方法將會在每次React
Native內容高度變化時進行呼叫。最後,我們設定根檢視的寬度和位置。注意我們也設定了高度,但是並沒有效果,因為我們已經將高度設定為根據RN內容進行彈性變化了。
你可以在這裡檢視完整的例子原始碼。
動態改變根檢視的彈性模式是可行的。改變根檢視的彈性模式將會導致佈局的重新計算,並且在重新量出內容尺寸時會呼叫rootViewDidChangeIntrinsicSize
方法。
注意:React Native佈局是通過一個特殊的執行緒進行計算,而原生UI檢視是通過主執行緒更新。這可能導致短暫的原生端和React Native端的不一致。這是一個已知的問題,我們的團隊已經在著手解決不同源的UI同步更新。 注意:除非根檢視成為其他檢視的子檢視,否則React Native不會進行任何的佈局計算。如果你想在還沒有獲得React Native檢視的尺寸之前先隱藏檢視,請將根檢視新增為子檢視並且在初始化的時候進行隱藏(使用
UIView
的hidden
屬性),然後在代理方法中改變它的可見性。