天天看點

基于 React Native 的 58 同城 App 開發實踐

作者簡介: 彭飛,58 同城 iOS 用戶端架構師。專注于新技術的研發,主要負責 App 端元件化架構以及性能優化,并已推廣 React Native 在 58 同城 App 中業務場景的應用。在 MDCC 2016 iOS 開發峰會上分享《58 同城 App 在 React Native 上的開發實踐-iOS 視角》主題演講。

導讀

React Native 在 iOS 界早就炒得火熱了,随着 2015 年底 Android 端推出後,一套代碼能運作于雙平台上,真正擁有了 Hybrid 架構的所有優勢。再加上 Native 的優秀性能,讓越來越多的公司在實際項目中一探究竟。58 同城 App 釋出子產品年代久遠,一直計劃進行重構以适應日益苛刻的使用者體驗,這個需求與我們在 React Native 上一探究竟的意願一碰撞,就産生了 React Native 在 58 App 的開發實踐。

本文重點介紹的是實踐過程中的技術架構和 Native 元件層以及熱更新平台的基本情況,以期能對 React Native 的從零到深入有一個整體的把握。

工欲善其事,必先利其器

React Native 是一項全新的技術,不同公司使用有不同的體驗,好壞衆說紛纭。基于此,必須根據自身的情況進行摸底調研。58 App 的調研過程從 2015 年 6 月就開始了,那時候 Android 還沒推出,僅調研了 iOS 的相關情況。真正的全面調研展開是在 2016 年 3 月開始的,整個過程持續到 5 月初結束。下面分三個階段介紹一下58 App 調研的具體曆程。

iOS RN 調研(2015.6)

React Native 确切的說從 2015 年開始在國内火起來的。牆外開花,牆内結果,國外技術研發,國内炒得火熱。阿裡天貓在這一方面走的比較靠前,但這時候 Android 部分還未推出,僅有 iOS。當時我們是拿二手車的清單頁進行的試驗,主要測試用 RN 實作的清單頁和用 Native 實作的清單頁在性能上的差别,當時得出的調研結論如下:

  1. 內建 React Native 需要從 iOS 7.0 開始,在 7.0 以下會因私有 API 問題在稽核過程中被拒;
  2. 性能方面,通過對 ListView 的針對性分析,在資料量不大的情況(50 條左右),記憶體和 CPU 的差别在 iPhone 4S 以上的裝置上可以接受;當資料量比較多,比如試驗過程中的 150 條,記憶體比較大,在低端裝置(4S/5C)上随着業務的擴充,性能會有瓶頸。
  3. 開發學習成本上,上手會比較快。但在開發的過程中遇到一些複雜的業務邏輯,得基于現有的架構擴充元件;還有在崩潰的收集上會比較麻煩,隻能定位到 OC 層的代碼,對于 JS 的運作時崩潰,目前的崩潰收集系統還無法采集。

當然,React Native 的理念是比較好的,既能擁有 Native 的良好使用者體驗,又能具備 Web 的快速釋出和疊代的功能。如果 Android 後續能很好推出,還能實作跨平台的“一處編寫,多處運作”的效果。無論內建與否,後續要持續關注,保持前沿技術的敏感性。對應 ListView 性能問題,RN 官方一直沒有一個很好的解決方案,我們最近也在做一些調研群組件的重新封裝,期望能從根本上解決這個問題。

雙平台RN基礎調研(2016.3)

在 2015 年底,React Native 就推出了 Android 版本,然後就有很多公司在開始嘗試了。春節流量高峰一過,上面就在籌劃 RN 上開發嘗試的事了。大體方向是以 App 中的釋出子產品做為試點,然後我們調研的技術偏向于釋出子產品的相關功能實作。調研由無線的總監專門組織,iOS/Android/JS 分别出一個人,成立了調研三人組,每周彙報進度。3 月份的調研主要面向的是RN基礎調研,摘取了其中的一些調研細節:

  1. Android/iOS 如何将RN內建到目前項目中?
  2. 如何用 RN 提供的原生元件實作釋出界面?
  3. 寫完的 JS 如何打包給 Native 使用?
  4. 由于是內建到已有項目,如何處理項目中的統一導航和 RN 提供的導航?
  5. 釋出表單中圖檔區域如何處理?Native 封裝的元件粒度如何?
  6. 釋出頁面的 UI 是用 ScrollView 控制還是 ListView 控制?

3 月份的調研,在 RN 的應用層面做到了一個心中有數,為後期的技術工作開展奠定了一個很好的基礎。至于基礎調研過程中的問題,限于篇幅問題,就不一一展開叙述了,有興趣的同學可以私下交流。

RN 熱更新調研(2016.4)

熱更新調研是整個調研最最關鍵的一環,因為官方并沒有熱更新的成熟方案。整個 4 月份一直在進行熱更新的調研,直到 5 月 8 日結束。熱更新調研主要涉及的主題為:

  1. 熱更新 Native 端的流程?如何控制熱更新包的大小及内置的資源大小?
  2. Server 端熱更新 diff 檔案存儲方案及更新方案?
  3. Native 端如何擷取檔案的更新?
  4. 異常復原機制?
  5. 是基于二進制算法的 diff 還是基于檔案算法的 diff?

熱更新中涉及的細節真的很多,上面隻是列出其中的一些。我們的調研過程,也是内部一遍遍技術評審/修改/再評審的過程。在下一章節會對這裡提到的主要問題進行分析和解釋。

萬事具備,水滴石穿

5月份 PM 已經陸續把需求整理完成了,然後成立了項目組,加入了釋出業務的 FE 及 Server。項目代号為“水滴”,無線 FE同學的創意。水滴,源自于三體,多元空間武器,通過量子糾纏進行超遠距離通訊和控制。React Native 如同水滴,對 JS-Native 通訊和控制。另外,寓意水滴石穿,堅持不懈,終能成功!

基于 RN 的移動 App 開發架構

基于 React Native 的 58 同城 App 開發實踐

首先從整體上了解一下基于 RN 的 App 開發架構。架構共分為五個部分:Native 元件/API 層、JS 中間層、JS 業務層、視圖載體頁、熱更新平台。JS 業務層、JS 中間層、Native 元件/API 層三者運作于視圖載體頁中,且 JS 業務層和 JS 中間層的代碼更新是通過熱更新平台更新到使用者手機應用中的。Native 元件/API 層是整個裝置的基石,JS 業務層通過 JS 中間層調用 Native 元件與 API。

Native 元件/API 層與 JS 中間層是無狀态,可以被複用的,它們被不同業務調用群組裝,能形成不同的業務功能。在這裡,一切業務都是基于元件的,任何業務的形成,都是調用 Native 元件及 API 來的。尤其是引入了 JS 中間層,不僅抹平了在不同平台(iOS/Android)上調用元件的差異性,還解耦了 JS 業務層與 Native 元件層。如果沒有 JS 中間層,Native 一個元件或者 API 的變動,都需要通知所有的業務方去進行修改,在業務到達一定量的情況下,這種改動不僅費時費力還具有風險,會影響線上功能。引入了 JS 中間層之後,Native 元件及 API 的變動,都在 JS 中間層進行處理,JS 業務層毫無感覺。

下面對這五個部分進行分别介紹:

Native 元件/API 層

Native 元件/API 層是在整個架構的最底層,也是整個裝置的基礎。

在這一層,除了 React Native 本身提供的原生元件外,我們還對沒有覆寫到的元件進行了封裝。React Native 提供的元件有 Image、ListView、Picker、Text、TextInput、ScrollView 等,具體可從 React Native 官方網站上查詢。我們擴充的元件有:支付、語音、彈窗、單選選擇器、多選無關聯選擇器、登入等。

在 React Native 中,除了元件,還有 API。官方提供的 API 有 ClipBoard、AsyncStorage、AppRegistry、Alert 等,更多完備的 API 可從 React Native 官方網站上查詢。我們擴充的 API 有:跳轉、定位、埋點、初始化參數等。

這些擴充的元件和 API 使得用 React Native,來實作本地化的業務成為了可能。當然随着業務的逐漸擴大,還會不斷豐富元件/API 庫,以适應業務的特殊性和多樣性。具體自定義元件情況如下圖:

基于 React Native 的 58 同城 App 開發實踐

以彈窗 dialog 元件為例,Native 與 JS 互動的協定為:

基于 React Native 的 58 同城 App 開發實踐

JS調用的示例為:

基于 React Native 的 58 同城 App 開發實踐

JS 中間層

JS 中間層是非常關鍵的一層,是為上文中擴充的 Native 元件/API 來服務的。JS 中間層如上文所述,不僅能抹平在不同平台上調用 Native 元件/API 的差異,還解耦了 JS 業務層與 Native 元件/API 層。

基于 React Native 的 58 同城 App 開發實踐

上圖所示的是在自定義彈窗元件(Dialog)中的代碼片段,從代碼的 95 行到 102 行,所做的是處理 iOS/Android 兩個平台上彈窗界面确定按鈕放置的位置不同而做的差異化處理。類似這些平台差異化的内容在實際開發中會有很多,如果這些差異都有業務方去做,不僅代碼可複用性差,而且耗時耗力,每一個新接入方都要重新開發調試。

至于 JS 業務層與 Native 元件/API 層之間的耦合關系,可以試想,如果沒有中間層的封裝,以上文的 dialog 元件為例,在業務層中将會,其中 WBCustomDialogManager 是 Native 的元件辨別别,還有 show 函數的相關參數存在多份這樣相同的代碼。這些與 Native 相關的内容如果發生變化,則所有與這個元件相關的業務都要更改。而引入了中間層之後,業務調用方,将不再關注這些細節,中間層在其中做了解藕。即使 Native 發生了變動,也會最大限度降低業務層的變動。

JS業務層

JS 業務層主要專注了業務的實作,包括視圖的渲染、元件的串聯、UI樣式的設定、Server API接口的調用與資料的處理。JS 業務層在改裝置中是最終代碼的落地,視圖載體頁加載的視圖以及熱更新系統更新的代碼都是直接針對 JS 業務層的,隻是這時業務層引用了 JS 中間層的代碼來實作對 Native 元件的調用。

視圖載體頁

視圖載體頁在這裡扮演了很重要的角色,是所有業務的一個統一載體。以 58同城 App 為例,裡面有大類頁/清單頁/詳情頁/釋出等不同形态的各種業務。通用的做法每一個業務一個載體頁。因為載體頁是 Native 代碼寫的,這使得當需要擴充一個業務線的時候,必須依賴發版。而統一了載體頁後,隻需要通過熱更新平台将 JS 代碼更新到 App 本地即可實作。

視圖載體頁單一載體功能的實作,很關鍵的部分在于跳轉到載體頁跳轉協定的設計。由于跳轉協定與具體的業務關聯較大,我們的跳轉協定中有一個重要的參數 pagetype,在這裡我們将 pagetype 設定為RN,而不是 list(清單頁)/detail(詳情頁)等與業務相關的類型。這樣在跳轉入口,伺服器進行配置的時候,不需要維護到特定載體頁的映射,從根本上解除了因業務變動帶來的跳轉配置耦合。

跳轉協定在不同的 App 中,實作思路不同,有很多 App 采用的 URL 形式來實作,但具體思路與上文描述的 JSON 形式相同。

熱更新平台

熱更新平台是整個架構的核心。熱更新平台的主要功能是将JS業務層及其引用的代碼編譯 link 好的 JSBundle 下載下傳到 Native App 中。在此過程中,需要控制更新檔案的大小以及失敗情況的處理。

熱更新平台涉及 JSBundle 資源管理系統(Server)、JSBundle 資料接口層(Server)、JSBundle Native 更新及管理層(Native)。JSBundle 資源管理系統負責将相關 JS 業務層代碼編譯 link 成 JSBundle 檔案,并将相關更新寫到一個資料緩存中心(例如 Redis 或 Memcached)。當 Native 通過 JSBundle 資料接口層提供的接口擷取對應的 JSBundle 的資訊時,資料接口層将從資料緩存中心查詢資料并傳回給 Native 端。Native 端為了提高使用者體驗,會對 JSBundle 進行緩存,使用者通路相關頁面的時候,先展示緩存,再通路接口,看是否有更新。

熱更新這塊,在這裡我從另外一個角度來闡述,即我們為什麼不用現有市面上的方案,而要自己搞一套。下面的三個章節來逐漸叙述。

熱更新的三個主要問題

熱更新現在公開的兩個方案是微軟的 Code Push 和 React Native 中文網中的 react-native-pushy。這兩種方案實作思路其實差不多,但針對我們的 App,不能滿足以下情況:

1. 内置資源體積過大,導緻整個應用包的大小過大,導緻過多占用使用者手機容量以及下載下傳應用耗時超長。

在 App 送出給應用商店稽核時,會将 RN 內建編譯後的 JSBundle 打進内置資源裡面去,而一個完整的 JSBundle 在區分平台(iOS/Android)以及 JS 壓縮的前提下,體積有 600K 左右。如果随着業務的快速擴充,假設有 100 個 JSBundle 的内置資源,那麼大小就會達到 60M。而應用商店的 App 大小,以 58App 為例,大小才 100M。内置資源過大,導緻整個應用包的體積過大,一是占用使用者手機容量,另一個每次下載下傳應用耗時超長。這在一定程度上是很難接受的。

2. 計算增量的基準檔案不唯一,平均合并的 diff 個數過多,增加了伺服器處理增量的複雜度和降低了 App 端合并 diff 性能。

Code Push 和 react-native-pushy 在利用 bsdiff 算法計算增量時,是相鄰兩個版本檔案的 diff。現在假設使用者本地 App 檔案版本是 1.0,而伺服器最新檔案版本是 4.0,則伺服器需要傳回App 3 個 diff(1.0 至 2.0 一個 diff, 2.0 至 3.0 一個 diff,3.0 至 4.0 一個 diff)。是以,由于 bsdiff 算法計算增量的基準檔案不唯一,導緻平均需合并的 diff 個數過多。這不僅增加了伺服器處理增量的複雜度,還降低了 App 端合并 diff 的性能,合并時間多長,阻塞使用者操作。

3. 将整個 App 的所有 JSBundle 檔案打包進行更新,不區分業務。導緻如果一個業務的 JSBundle 有問題,會影響其他業務 JSBundle 的正常運作。

Code Push 和 react-native-pushy 做的是一個通用的熱更新平台,一個 App 有一個 key,檔案更新以此 key 為辨別,所有檔案在一個 zip 包裡面,不區分業務。目前稍微大的網際網路公司,都是以業務線劃分職能的,在技術架構上,各業務線業務應該做到互相不幹擾。react-native-pushy 這種不區分業務的更新模式,會導緻如果一個業務的 JSBundle 有問題,會影響其他業務 JSBundle 的正常更新,造成業務的互相幹擾。

基于對上面的分析,我們得出了熱更新需要解決的三個問題:

  1. 既要控制更新包的大小,又要控制内資資源的大小;
  2. 降低伺服器處理增量複雜度和提高 App 端合并 Diff 性能;
  3. Diff 更新以 JSBundle 檔案為機關,業務 Diff 之間互相不幹擾。

熱更新的解決方案

基于上面的三個問題,我們有如下的解決方案:

JSBundle拆分及公共部分生成

基于 React Native 的 58 同城 App 開發實踐

在介紹 diff 生成及合并算法之前,先介紹一下一個關鍵性要點。即我們重點關注 React Native 中 JSBundle 的内容的特殊性,發現打包編譯後的一個 JSBundle 可以拆成一個穩定的公共部分加上差異部分。如上圖所示,針對一個入口檔案 pageIndex.jsbundle, 可以拆分成穩定的 commonPart.jsbundle 以及差異部分 diffPart.jsbundle。其中,commonPart.jsbundle 與具體業務無關,是 React Native 中一些公用的 JS 庫。

commonPart.jsbundle 生成的方法為(以 iOS 為例,Android 的原理相同):

1. 建立一個blank.ios.js檔案,在檔案中僅需引入react及react-native,注意不要包含任何業務代碼,具體代碼如下截圖:

基于 React Native 的 58 同城 App 開發實踐

2. 通過curl指令将blank.ios.js檔案編譯成common.ios.jsbundle。筆者在本地的執行指令為:

curl 'http://localhost:/blank.ios.bundle?minify=true&dev=false' -o common.ios.jsbundle
           

得到的common.ios.jsbundle結果如下圖所示:

基于 React Native 的 58 同城 App 開發實踐

需要補充的是,因為 commonPart.jsbundle 依賴 Native 代碼,是以 commonPart.jsbundle 的更新是跟着 App 發版走的。

Diff的生成與合并

基于上文對 jsbundle 的拆分,我們選擇了 google-diff-match-patch 算法生成diff 及合并 diff。在計算diff時,以commonPart.jsbundle為基準,計算目前版本的pageIndex.jsbundle與commonPart.jsbundle之間的文本差異,然後APP端拿到文本差異描述後,再利用google-diff-match-patch算法将文本差異合并到本地的commonPart.jsbundle中去。

生成diff調用google-diff-match-patch的API為(iOS端為例,其他端可找到對應API):

基于 React Native 的 58 同城 App 開發實踐

合并diff調用google-diff-match-patch的API為(iOS端為例,其他端可找到對應API):

基于 React Native 的 58 同城 App 開發實踐

熱更新流程

基于 React Native 的 58 同城 App 開發實踐

熱更新流程如上圖所示。圖中載體頁是指native加載React Native代碼的一個載體。RN是React Native的縮寫。下面對上述流程進行叙述:

1. 進入載體頁,判斷是否有緩存。

進入載體頁之後,會先判斷是否有RN緩存,如果有緩存,則直接進行下一步。如果沒有緩存,則去伺服器下載下傳對應的RN資源(RN資源指的RN代碼檔案)。

2. 展示RN頁面

根據RN資源,載體頁渲染出對應的頁面。

3. 背景請求目前頁面最新資訊

在展示完RN頁面後,新起子線程在背景向伺服器請求目前頁面的最新資訊資料。

4. 根據最新資訊進行更新操作

根據上一步伺服器傳回的資料,進行分支操作:如果是強制更新,則彈窗提示使用者需要強制重新整理目前頁面才能繼續操作;如果是一般的非強制更新,則程式在背景更新資料,使用者下次進入此頁面後更新生效;如果沒有更新,則不做任何操作,流程結束。

針對上述流程有一個補充:在APP啟動的時候向伺服器請求預加載資料,提前對一些重要的RN資源進行加載,這樣在上述流程的第一步就可以直接利用RN緩存快速進入頁面,使用者體驗會更好。

結果分析

基于上述方案,可解決上述提到的三個問題:

  1. 在内置資源的時候,隻需内置一分commonPart.jsbundle和相應入口頁面對應的diffPart.jsbundle。在通常情況下,commonPart.jsbundle占整個jsbundle近2/3的大小,這相比基于react-native-pushy而内置的資源節省了近2/3的大小。針對業務疊代過程的更新包,都隻是業務代碼的更新,包的大小也得到了很好的控制。另外,由于RN的接口隻能加載一個合并後的完整的jsbundle,但這個完整的jsbundle我們是實時合并的,是不存儲到檔案系統的,隻在記憶體中操作。通過實際運作,這種合并耗時時間很少,基本可忽略不計。這樣,即使app運作相對長的一段時間,也不會增加包的大小的。
  2. 伺服器計算diff包的時候,由于隻需對commonPart.jsbundle進行比較,是以計算增量的複雜度相比react-native-pushy降到了最低。APP端在合成diff的時候,隻需要将一個common.jsbundle與一個diff.jsbundle進行合并即可,合并性能相比react-native-pushy平均要合并多個,得到了很大的提高。
  3. 本方案計算的更新,以某一個入口頁面對于的pageIndex.jsbundle為機關來操作,是以具體業務為機關的。每一個pageIndex.jsbundle對應的更新都是互相獨立的,即使一個業務更新出錯,也不會影響到其他業務的更新。

産品順利上線,幸有PM燒高香

經過兩個多月的研發,Native端,FE中間層,FE業務層,業務Server,熱更新平台所有功能全部上線了,多虧PM上線前燒了高香,整個過程是很曲折的,但結果是美好的。但老闆說了,RN這個東西從來沒上過,萬一上線之後出了重大問題怎麼辦?通過發版解決周期太長,速度太慢。于是乎隻能通過Server端來控制了。

通過Server控制線上出現問題的風險,我們稱為降級政策。即用RN之前,58APP使用的Hybrid架構來做的釋出頁面。如果線上的RN出了問題,通過Server端控制跳轉協定,讓跳轉到web的釋出頁面上。等待RN問題解決了,再行切換。

産品上線後,産品層面最關注的就是加載速度了。針對釋出功能來說,從Hybrid切換到React Native,為的就是加載速度。由于RN有熱更新,基本上使用者是在90%的情況下進入有緩存的界面的。下圖是由QA組同學提供的雙平台在加載時間上的性能測試結果(有緩存的情況下)。

基于 React Native 的 58 同城 App 開發實踐

上圖中的Web頁面加載時間是在網絡狀況良好的情況下的資料。從圖中可以看出,RN頁面基本上是秒進,這讓從點選釋出按鈕到展示釋出頁面期間的使用者流失基本降到了最低。

經過項目組成員的辛苦努力,React Native在58 App上算是邁出了第一步,官方SDK現在是一個星期一個版本的更新節奏,後期開發中肯定還會有好多坑,躍坑的過程肯定很精彩!

了解最新移動開發相關資訊和技術,請關注mobilehub公衆微信号(ID: mobilehub)。

基于 React Native 的 58 同城 App 開發實踐

繼續閱讀