[譯] Airbnb 在 React Native 上下的賭注(二):技術細節

Airbnb 中的 React Native:技術部分

技術細節

這是系列博客文章中的第二篇,本文將會概述咱們使用 React Native 的經驗,以及 Airbnb 移動端接下來要作的事情。javascript

React Native 自己在 Android,iOS,Web 和跨平臺框架的各個部分上,是一個相對較新且快速迭代的平臺。兩年後,咱們能夠頗有把握地說,React Native 在不少方面都是革命性的。這是移動設備的轉變範例,咱們可以從衆多目標中得到收益。然而,它的好處並不是沒有明顯的痛點。css

技術性優點

跨平臺

React Native 的主要好處在於,你所編寫的代碼可以同時在 Andriod 和 iOS 上運行。使用 React Native 的大多數功能均可以實現 95 - 100% 的共享代碼,和 0.2% 不一樣平臺須要用的的文件.android.js/.ios.js)。html

統一設計語言系統(DLS)

咱們開發了一種名爲 DLS 的跨平臺設計語言。同時擁有每一個組件的 Android、iOS、React Native 和 Web 版本。擁有統一的設計語言能夠實現編寫跨平臺代碼的功能,這意味着設計,組件名稱和屏幕能夠跨平臺保持一致。可是,咱們仍然可以在適用的狀況下,作出適合平臺的決策。例如,咱們在 Andriod 上使用原生的 Toolbar,iOS 上使用 UINavigationBar,但咱們須要在 Andriod 上隱藏 disclosure indicators,由於這不符合 Andriod 平臺設計準則。前端

咱們選擇了重寫組件,而不是封裝原生組件。由於每一個平臺分別製做適用的 API 會更加可靠,而且能夠減小 Android 和 iOS 工程師的維護開銷,他們可能不清楚應該如何正確測試 React Native 中更改的代碼。可是,它確實會致使同一組件的原生和 React Native 版本不一樣步的平臺之間出現碎片。java

React

React 成爲最受開發者歡迎的 Web 框架也是有緣由的。那就是它很是簡單但功能強大,適用於大型代碼庫。咱們很是喜歡的特色是:node

  • 組件: React 組件經過明肯定義的屬性和狀態強制分離關注點。React 的可擴展性在其中起着很大做用。
  • 簡單的生命週期: 簡單來講, Android 和 iOS 的生命週期都出了名的複雜。函數式和響應式 React 組件從根本上解決了這個問題,因此學習 React Native 比學習 Andriod 和 iOS 更簡單。
  • 聲明式: React 的聲明特性幫助咱們的 UI 與基礎狀態保持同步。

迭代速度

在使用 React Native 進行開發時,咱們可以穩定地使用熱加載來測試咱們在 Android 和 iOS 上的改動,過程只需一兩秒鐘的時間。即便原生應用不遺餘力也無法達到 React Native 實現的迭代速度。在最理想的狀況下,原生編譯時間爲 15 秒,但對於完整項目的構建,竟高達 20 分鐘。react

基礎框架的投入

咱們開發了普遍的集成到咱們的原生基礎框架,諸如網絡、國際化、實驗、共享元素轉換、設備信息,賬戶信息等許多核心組件都封裝在一個 React Native API 中。這些橋接是一些更復雜的部分,由於咱們想要將現有的 Android 和 iOS API 封裝成對 React 一致且規範的東西。儘管經過快速迭代和新基礎架構的開發,來保持這些橋接是最新的,但基礎架構團隊的投入能簡化產品工做。android

若是沒有大量投入基礎架構,React Native 會致使糟糕的開發人員和用戶體驗。所以,咱們不相信 React Native 能夠在沒有重大和持續投入的狀況下,直接應用到現有的應用程序中。ios

性能表現

性能是React Native 最大的問題之一。可是,實踐中遇到這個問題的機會不大。咱們的大多數使用了 React Native 的屏幕都像原生的同樣流暢。咱們每每會總在一個單一的維度中去考慮性能。咱們常常看到移動端工程師認爲 JS,「比Java慢」。然而,在不少狀況下,移動端主線程的業務邏輯和佈局均可以提升渲染性能。css3

當咱們確實發現性能問題時,大多數是由過分渲染引發的,這能夠經過有效地使用 shouldComponentUpdateremoveClippedSubviews,和使用 Redux 來解決。

然而,初始化和初識渲染時間(下面概述)使得 React Native 在啓動屏幕,Deep Links 表現不佳,而且在屏幕之間導航時增長了 TTI 時間。另外,由於 Yoga 在 React Native 組件和原生視圖之間進行了轉換,因此丟失幀的屏幕很難調試。

Redux

咱們使用了 Redux 進行狀態管理,發現這種方法很是有效。不但防止了 UI 與狀態不一樣步的問題,在屏幕之間也能輕鬆實現數據共享。可是,Redux 以其模板和相對困難的學習曲線而聲名狼藉。咱們爲一些常見模板提供了生成器,但它仍然是在使用 React Native 時,最具挑戰性的部分和混淆之一。值得注意的是,這些挑戰不是 React Native 特有的。

原生支持

因爲 React Native 中的全部內容均可以經過原生代碼進行橋接,所以咱們最終可以在開始時構建許多咱們不肯定的事情,例如:

  1. 共享元素轉換: 咱們構建了一個 組件,該組件由 Android 和 iOS 上的原生共享元素代碼所支持。這甚至適用於原生和 React Native 屏幕。
  2. Lottie: 經過在 Android 和 iOS 上封裝現有的庫,咱們可以讓 Lottie 在 React Native 中正常工做。
  3. Native 網絡棧: React Native 在兩個平臺上都使用咱們現有的原生網絡棧和緩存。
  4. 其它核心基礎架構: 就像網絡同樣,咱們將其餘現有的原生基礎架構(如國際化,實驗等)封裝起來,以便它可以在 React Native 中無縫工做。

靜態分析

咱們在網絡方面上使用 eslint 的歷史很是悠久,此次咱們也能夠利用它。不過,Airbnb 是開創 Prettier 的第一個平臺。咱們發現它能夠有效減小 PR 上的麻煩。如今,咱們的網絡基礎架構團隊正在積極研究 Prettier。

咱們還用分析來衡量渲染時間和性能,以肯定哪些屏幕是性能問題調查的首要任務。

因爲 React Native 比咱們的網絡基礎結構更小、更新,所以是良好的測試新想法的平臺。咱們爲 React Native 建立的許多工具和想法如今都被 Web 採用。

Animated

多虧了 React Native 動畫 庫,咱們可以實現流暢動畫,甚至交互驅動的動畫,如視差滾動。

JS/React 開源

因爲 React Native 會運行 React 和 JavaScript,所以咱們可以利用大量的 Javascript 項目,例如 Redux、Reselect、Jest 等。

Flexbox

React Native 使用了 Yoga 來處理佈局。這是個跨平臺的 C 語言庫,經過 flexbox API 處理佈局計算。早些時候,咱們受到 Yoga 的侷限,例如缺少長寬比,不過在後續更新增添了。此外,像 flexbox froggy 這樣有趣的教程,能讓你在上手的時候更加享受。

與 Web 互相協做

在 React Native 探索的後期,咱們開始一次性爲 Web,iOS 和 Android 構建項目。鑑於 Web 也使用 Redux,咱們發現大量代碼能夠在 Web 和原平生臺上共享,無需作任何更改。

缺點

React Native 還不成熟

React Native 相對 Android 或 iOS 來講,略顯不夠成熟。它很新,也有野心,而且迭代速度很是快。雖然 React Native 在大多數狀況下都能很好地工做,但有些狀況下,它的不成熟可能會顯現出來,使用原生就能垂手可得實現的事情變得很是困難。不幸的是,這些狀況很難預測,並且可能須要幾小時到幾天的時間才能解決。

維護 React Native 的分支

因爲 React Native 還不成熟,咱們有時須要修補 React Native 源碼。除了將問題反饋給 React Native 以外,咱們還必須維護一個分支,咱們能夠在其中快速合併更改並升級版本。在過去兩年中,咱們不得不在 React Native 添加大約 50 次 commit。這使得升級 React Native 的過程很是痛苦。

JavaScript 工具

JavaScript 是一種無類型語言。缺少類型安全既難以擴展,也成爲習慣於類型化語言的移動端工程師爭論的焦點,不然他們可能會對學習 React Native 感興趣。咱們探討了採用 flow 的方式,但隱晦的錯誤消息致使了使人沮喪的開發者體驗。咱們還探討了 TypeScript,但將其整合到咱們現有的基礎架構中時,如 babelmetro bundler 是有問題的。不過,咱們正在繼續積極研究 Web 上的 TypeScript。

重構

JavaScript 一個無類型的反作用是,重構很是困難且容易出錯。重命名一些屬性,特別是帶有通用名稱的屬性(如 onClick )或經過多個組件傳遞的屬性,對於準確地重構來講是一場噩夢。更糟糕的是,重構在生產環境中崩潰,而不是在編譯時,很難對其進行適當的靜態分析。

JavaScriptCore 不一致

React Native 的一個微妙和棘手的方面是,它須要在 JavaScriptCore 環境上執行。如下是咱們遇到的問題:

  • iOS 自帶的 JavaScriptCore 能夠開箱即用。這就是說,iOS 大部分是一致的,對咱們來講沒有問題。
  • Android 不帶有 JavaScriptCore,所以由 React Native 提供。可是,默認狀況下,得到的是舊版本的。這致使的結果是,咱們不得不本身捆綁一個新版本的 JavaScriptCore。
  • 在調試時,React Native 會鏈接到 Chrome 開發者工具。這點很是好,由於它是一個強大的調試器。可是,一旦鏈接了調試器,全部 JavaScript 都將在 Chrome 的 V8 引擎中運行。99.9% 的狀況都運行良好。可是,在一個實例中,咱們在 iOS 上使用 toLocaleString 時,咱們碰到了問題,調試只能在 Android 上工做。事實證實,Android (不包括 JSC),除非你正在調試,在這種狀況下,它使用的是 V8 引擎,不然它會悄無聲息地失敗。在不知道這些技術細節的狀況下,可能會致使產品工程師進行很多天痛苦的調試。

React Native 開源庫

學習平臺既困難又費時。大多數人只能很好地瞭解一或兩個平臺。React Native 庫有原生橋接,例如地圖,視頻等,開發者須要對三個平臺都有相同的認識才能實現。咱們發現大多數 React Native 開源項目都是由有一兩次經驗的人所編寫的。這致使了 Android 或 iOS 上的不一致或意想不到的錯誤。

在 Android 上,許多 React Native 庫也要求你使用 node_modules 的相對路徑,而不是發佈與社區所指望的不一致的 Maven Artifact。

並行基礎架構和工做

咱們在 Android 和 iOS 上積累了多年的原生基礎架構。可是,在 React Native 中,咱們從徹底空白的狀態開始,不得不編寫或建立全部現有基礎架構的橋接。這意味着,有時產品工程師須要一些尚不存在的功能。這種狀況,他們要麼在一個不熟悉的平臺上工做,要麼在項目範圍以外構建,或者乾等到有人建立這個功能。

崩潰監控

咱們使用 Bugsnag 進行 Android 和 iOS 崩潰報告。雖然 Bugsnag 一般在兩個平臺上能正常工做,但它不太靠譜,而且須要比在其餘平臺上作更多的工做。因爲 React Native 在行業中相對較新且罕見,所以咱們必須構建大量基礎架構,例如內部上傳的源地圖,而且必須與 Bugsnag 合做才能執行諸如發生在 React Native 過濾器崩潰等事件。

因爲 React Native 周圍的自定義基礎架構數量衆多,偶爾也會出現嚴重問題,例如未報告崩潰或源地圖未正確上傳。

最後,若是問題跨越 React Native 和原生代碼,調試 React Native 崩潰每每更具挑戰性,由於堆棧跟蹤不能在 React Native 和原生代碼之間跳轉。

原生橋接

React Native 有 橋接 API 用於原生和 React Native 之間進行通訊。雖然它能按預期正常工做,但編寫起來很是麻煩。首先,它要求全部三種開發環境都要正確設置。咱們也遇到了不少來自 JavaScript 的類型不正確的問題。例如,整數一般是由字符串封裝的,這個問題直到橋接後才能被察覺。更糟糕的是,有時 iOS 會悄無聲息地失敗,而 Android 則會崩潰。到 2017 年末,咱們開始研究從 TypeScript 定義自動生成的橋接代碼,但爲時已晚。

初始化時間

在 React Native 首次渲染以前,你必須初始化其運行時。不幸的是,即便在高端設備上,咱們的應用也須要幾秒鐘的時間。因此,幾乎是不可能使用 React Native 來啓動屏幕。咱們經過在應用程序啓動時初始化 React Native 來縮短第一次渲染時間。

初始渲染時間

與原生屏幕不一樣,渲染 React Native 須要至少一個完整的主線程 -> JS -> Yoga 佈局線程 -> 主線程返回以前,而後纔有足夠的信息來第一次渲染屏幕。咱們能夠看到 iOS 平均初始 p90 渲染 280ms,而 Android 須要 440ms。在 Android 上,咱們使用一般用於共享元素轉換的 postponeEnterTransition API 來延遲顯示屏幕直到它完成渲染。在 iOS 上,咱們遇到了問題,從 React Native 快速設置導航欄配置。所以,咱們爲全部 React Native 屏幕過渡添加了 50ms 的仿真延遲,以防止配置加載後導航欄閃爍。

App 大小

React Native 對應用程序大小也有不可忽視的影響。在 Android 上,React Native(Java + JS + 原生庫,例如 Yoga + Javascript 運行時)的總大小爲每一個 ABI 8MB。在一個 APK 中使用 x86 和 arm(僅 32 位),體積將接近 12MB。

64 位

因爲這個問題,咱們仍然不能在 Android 上安裝一個 64 位的 APK。

手勢

咱們避免在涉及複雜手勢的頁面上使用 React Native,由於 Android 和 iOS 的觸摸子系統很是不一樣,以致於整理出一套統一的 API 對整個 React Native 社區來講都具備挑戰性。然而,這項工做仍在繼續進行,react-native-gesture-handler 最近已經發布 1.0 版本。

List 太長

React Native 在這方面取得了一些進展,好比 FlatList 庫。可是,它們遠不及 Android 上的 RecyclerView 或是 iOS 上的 UICollectionView 的成熟度和靈活性。因爲多進程的問題,許多限制難以克服。Adapter 數據沒法同步訪問,這會致使視圖閃爍,由於它們在快速滾動時進行了異步渲染。另外,文本也沒法同步測量,所以 iOS 沒法使用預先計算的 cell 高度進行某些優化。

升級 React Native

儘管大多數 React Native 升級都很微不足道,但有一些卻使人很是痛苦。尤爲是, React Native 0.43(2017 年 4 月)至 0.49(2017 年 10月)版本幾乎沒法使用,由於其中使用了 React 16 alpha 和 beta。這是個大問題,由於大多數專爲 Web 使用而設計的 React 庫不支持之前發佈的 React 版本。爭論這次升級的適當依賴關係的過程,對 2017 年中其餘 React Native 基礎架構工做形成重大損害。

輔助功能

在 2017 年,咱們進行了一次輔助功能大修,其中咱們投入了大量的精力,確保殘疾人士可使用 Airbnb 預訂來知足他們的房源須要。可是,React Native 關於輔助功能的 API 有不少漏洞。爲了知足甚至最小可接受的輔助功能條,咱們必需要維護 React Native 的一個分支,來合併修復程序。對於這些狀況,Android 或 iOS 上的一行修復須要數天時間,才能肯定如何將其添加到 React Native,而後 cherry-pick,接着在 React Native Core 上提交問題,並在接下來的幾周內對其進行跟蹤。

棘手的崩潰

咱們不得不面對一些難以解決的、很是奇怪的崩潰。例如,咱們目前在 @ReactProp 註解中遇到了這個崩潰,而且沒法在任何設備上覆現,即便是和那些持續崩潰的設備具備相同硬件和軟件也是如此。

在 Android 上的 SavedInstanceState 跨進程

Android 常常會去清理後臺進程,但給了它們一個同步把狀態保存在 bundle 中的機會。可是,在 React Native 上,全部狀態只能在 JS 線程中訪問,所以沒法同步進行。即便狀況並不是如此,做爲狀態存儲的 Redux 與此方法也不兼容,由於它混合了包含可序列化和不可序列化數據,而且可能包含比 saveInstanceState 包中容納的更多類型的數據,這會致使在生產環境下崩潰


這是系列博客文章的第二部分,重點講述了咱們使用 React Native 的經驗,以及 Airbnb 移動端接下來要作的事情。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索