- 原文地址:React Native at Airbnb: The Technology
- 原文作者:Gabriel Peal
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:ALVINYEH
- 校對者:ssshooter、LeeSniper
Airbnb 中的 React Native:技術部分
技術細節
這是系列部落格文章中的第二篇,本文將會概述我們使用 React Native 的經驗,以及 Airbnb 移動端接下來要做的事情。
React Native 本身在 Android,iOS,Web 和跨平臺框架的各個部分上,是一個相對較新且快速迭代的平臺。兩年後,我們可以很有把握地說,React Native 在很多方面都是革命性的。這是移動裝置的轉變範例,我們能夠從眾多目標中獲得收益。然而,它的好處並非沒有明顯的痛點。
技術性優勢
跨平臺
React Native 的主要好處在於,你所編寫的程式碼能夠同時在 Andriod 和 iOS 上執行。使用 React Native 的大多數功能都可以實現 95 - 100% 的共享程式碼,和 0.2% 不同平臺需要用的的檔案(.android.js/.ios.js)。
統一設計語言系統(DLS)
我們開發了一種名為 DLS 的跨平臺設計語言。同時擁有每個元件的 Android、iOS、React Native 和 Web 版本。擁有統一的設計語言可以實現編寫跨平臺程式碼的功能,這意味著設計,元件名稱和螢幕可以跨平臺保持一致。但是,我們仍然能夠在適用的情況下,做出適合平臺的決策。例如,我們在 Andriod 上使用原生的 Toolbar,iOS 上使用 UINavigationBar,但我們需要在 Andriod 上隱藏 disclosure indicators,因為這不符合 Andriod 平臺設計準則。
我們選擇了重寫元件,而不是封裝原生元件。因為每個平臺分別製作適用的 API 會更加可靠,並且可以減少 Android 和 iOS 工程師的維護開銷,他們可能不清楚應該如何正確測試 React Native 中更改的程式碼。但是,它確實會導致同一元件的原生和 React Native 版本不同步的平臺之間出現碎片。
React
React 成為最受開發者歡迎的 Web 框架也是有原因的。那就是它非常簡單但功能強大,適用於大型程式碼庫。我們非常喜歡的特點是:
- 元件: React 元件通過明確定義的屬性和狀態強制分離關注點。React 的可擴充套件性在其中起著很大作用。
- 簡單的生命週期: 簡單來說, Android 和 iOS 的生命週期都出了名的複雜。函式式和響應式 React 元件從根本上解決了這個問題,所以學習 React Native 比學習 Andriod 和 iOS 更簡單。
- 宣告式: React 的宣告特性幫助我們的 UI 與基礎狀態保持同步。
迭代速度
在使用 React Native 進行開發時,我們能夠穩定地使用熱載入來測試我們在 Android 和 iOS 上的改動,過程只需一兩秒鐘的時間。即使原生應用竭盡全力也沒法達到 React Native 實現的迭代速度。在最理想的情況下,原生編譯時間為 15 秒,但對於完整專案的構建,竟高達 20 分鐘。
基礎框架的投入
我們開發了廣泛的整合到我們的原生基礎框架,諸如網路、國際化、實驗、共享元素轉換、裝置資訊,帳戶資訊等許多核心元件都封裝在一個 React Native API 中。這些橋接是一些更復雜的部分,因為我們想要將現有的 Android 和 iOS API 封裝成對 React 一致且規範的東西。儘管通過快速迭代和新基礎架構的開發,來保持這些橋接是最新的,但基礎架構團隊的投入能簡化產品工作。
如果沒有大量投入基礎架構,React Native 會導致糟糕的開發人員和使用者體驗。因此,我們不相信 React Native 可以在沒有重大和持續投入的情況下,直接應用到現有的應用程式中。
效能表現
效能是React Native 最大的問題之一。但是,實踐中遇到這個問題的機會不大。我們的大多數使用了 React Native 的螢幕都像原生的一樣流暢。我們往往會總在一個單一的維度中去考慮效能。我們經常看到移動端工程師認為 JS,“比Java慢”。然而,在很多情況下,移動端主執行緒的業務邏輯和佈局都可以提高渲染效能。
當我們確實發現效能問題時,大多數是由過度渲染引起的,這可以通過有效地使用 shouldComponentUpdate,removeClippedSubviews,和使用 Redux 來解決。
然而,初始化和初識渲染時間(下面概述)使得 React Native 在啟動螢幕,Deep Links 表現不佳,並且在螢幕之間導航時增加了 TTI 時間。另外,因為 Yoga 在 React Native 元件和原生檢視之間進行了轉換,所以丟失幀的螢幕很難除錯。
Redux
我們使用了 Redux 進行狀態管理,發現這種方法非常有效。不但防止了 UI 與狀態不同步的問題,在螢幕之間也能輕鬆實現資料共享。但是,Redux 以其模板和相對困難的學習曲線而聲名狼藉。我們為一些常見模板提供了生成器,但它仍然是在使用 React Native 時,最具挑戰性的部分和混淆之一。值得注意的是,這些挑戰不是 React Native 特有的。
原生支援
由於 React Native 中的所有內容都可以通過原生程式碼進行橋接,因此我們最終能夠在開始時構建許多我們不確定的事情,例如:
- 共享元素轉換: 我們構建了一個 元件,該元件由 Android 和 iOS 上的原生共享元素程式碼所支援。這甚至適用於原生和 React Native 螢幕。
- Lottie: 通過在 Android 和 iOS 上封裝現有的庫,我們能夠讓 Lottie 在 React Native 中正常工作。
- Native 網路棧: React Native 在兩個平臺上都使用我們現有的原生網路棧和快取。
- 其它核心基礎架構: 就像網路一樣,我們將其他現有的原生基礎架構(如國際化,實驗等)封裝起來,以便它能夠在 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,但將其整合到我們現有的基礎架構中時,如 babel和 metro 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 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。