1. 程式人生 > >在原生和React Native間通訊

在原生和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方法中呼叫的那個)的componentWillReceivePropscomponentWillUpdateProps方法在屬性更新後不會觸發。但是,你可以通過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和原生中都設定彈性尺寸可能導致不確定的行為。比如--不要在設定RCTRootViewRCTRootViewSizeFlexibilityWidth時同時指定最頂層的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檢視的尺寸之前先隱藏檢視,請將根檢視新增為子檢視並且在初始化的時候進行隱藏(使用UIViewhidden屬性),然後在代理方法中改變它的可見性。