基于 React Native 的 58 同城 App 開發實踐
導讀
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 實現的列表頁在性能上的差別,當時得出的調研結論如下:
-
集成 React Native 需要從 iOS 7.0 開始,在 7.0 以下會因私有 API 問題在審核過程中被拒;
-
性能方面,通過對 ListView 的針對性分析,在數據量不大的情況(50 條左右),內存和 CPU 的差別在 iPhone 4S 以上的設備上可以接受;當數據量比較多,比如試驗過程中的 150 條,內存比較大,在低端設備(4S/5C)上隨著業務的擴展,性能會有瓶頸。
-
開發學習成本上,上手會比較快。但在開發的過程中遇到一些復雜的業務邏輯,得基于現有的框架擴展組件;還有在崩潰的收集上會比較麻煩,只能定位到 OC 層的代碼,對于 JS 的運行時崩潰,目前的崩潰收集系統還無法采集。
當然,React Native 的理念是比較好的,既能擁有 Native 的良好用戶體驗,又能具備 Web 的快速發布和迭代的功能。如果 Android 后續能很好推出,還能實現跨平臺的“一處編寫,多處運行”的效果。無論集成與否,后續要持續關注,保持前沿技術的敏感性。對應 ListView 性能問題,RN 官方一直沒有一個很好的解決方案,我們最近也在做一些調研和組件的重新封裝,期望能從根本上解決這個問題。
雙平臺RN基礎調研(2016.3)
在 2015 年底,React Native 就推出了 Android 版本,然后就有很多公司在開始嘗試了。春節流量高峰一過,上面就在籌劃 RN 上開發嘗試的事了。大體方向是以 App 中的發布模塊做為試點,然后我們調研的技術偏向于發布模塊的相關功能實現。調研由無線的總監專門組織,iOS/Android/JS 分別出一個人,成立了調研三人組,每周匯報進度。3 月份的調研主要面向的是RN基礎調研,摘取了其中的一些調研細節:
-
Android/iOS 如何將RN集成到當前項目中?
-
如何用 RN 提供的原生組件實現發布界面?
-
寫完的 JS 如何打包給 Native 使用?
-
由于是集成到已有項目,如何處理項目中的統一導航和 RN 提供的導航?
-
發布表單中圖片區域如何處理?Native 封裝的組件粒度如何?
-
發布頁面的 UI 是用 ScrollView 控制還是 ListView 控制?
3 月份的調研,在 RN 的應用層面做到了一個心中有數,為后期的技術工作開展奠定了一個很好的基礎。至于基礎調研過程中的問題,限于篇幅問題,就不一一展開敘述了,有興趣的同學可以私下交流。
RN 熱更新調研(2016.4)
熱更新調研是整個調研最最關鍵的一環,因為官方并沒有熱更新的成熟方案。整個 4 月份一直在進行熱更新的調研,直到 5 月 8 日結束。熱更新調研主要涉及的主題為:
-
熱更新 Native 端的流程?如何控制熱更新包的大小及內置的資源大小?
-
Server 端熱更新 diff 文件存儲方案及更新方案?
-
Native 端如何獲取文件的更新?
-
異常回滾機制?
-
是基于二進制算法的 diff 還是基于文件算法的 diff?
熱更新中涉及的細節真的很多,上面只是列出其中的一些。我們的調研過程,也是內部一遍遍技術評審/修改/再評審的過程。在下一章節會對這里提到的主要問題進行分析和解釋。
萬事具備,水滴石穿
5月份 PM 已經陸續把需求整理完成了,然后成立了項目組,加入了發布業務的 FE 及 Server。項目代號為“水滴”,無線 FE同學的創意。水滴,源自于三體,多維空間武器,通過量子糾纏進行超遠距離通訊和控制。React Native 如同水滴,對 JS-Native 通訊和控制。另外,寓意水滴石穿,堅持不懈,終能成功!
基于 RN 的移動 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 庫,以適應業務的特殊性和多樣性。具體自定義組件情況如下圖:
以彈窗 dialog 組件為例,Native 與 JS 交互的協議為:
JS調用的示例為:
JS 中間層
JS 中間層是非常關鍵的一層,是為上文中擴展的 Native 組件/API 來服務的。JS 中間層如上文所述,不僅能抹平在不同平臺上調用 Native 組件/API 的差異,還解耦了 JS 業務層與 Native 組件/API 層。
上圖所示的是在自定義彈窗組件(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 的正常更新,造成業務的相互干擾。
基于對上面的分析,我們得出了熱更新需要解決的三個問題:
-
既要控制更新包的大小,又要控制內資資源的大小;
-
降低服務器處理增量復雜度和提高 App 端合并 Diff 性能;
-
Diff 更新以 JSBundle 文件為單位,業務 Diff 之間相互不干擾。
熱更新的解決方案
基于上面的三個問題,我們有如下的解決方案:
JSBundle拆分及公共部分生成
在介紹 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,注意不要包含任何業務代碼,具體代碼如下截圖:
2. 通過curl命令將blank.ios.js文件編譯成common.ios.jsbundle。筆者在本地的執行命令為:
curl 'http://localhost:8081/blank.ios.bundle?minify=true&dev=false' -o common.ios.jsbundle
得到的common.ios.jsbundle結果如下圖所示:
需要補充的是,因為 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):
合并diff調用google-diff-match-patch的API為(iOS端為例,其他端可找到對應API):
熱更新流程
熱更新流程如上圖所示。圖中載體頁是指native加載React Native代碼的一個載體。RN是React Native的縮寫。下面對上述流程進行敘述:
1. 進入載體頁,判斷是否有緩存。
進入載體頁之后,會先判斷是否有RN緩存,如果有緩存,則直接進行下一步。如果沒有緩存,則去服務器下載對應的RN資源(RN資源指的RN代碼文件)。
2. 展示RN頁面
根據RN資源,載體頁渲染出對應的頁面。
3. 后臺請求當前頁面最新信息
在展示完RN頁面后,新起子線程在后臺向服務器請求當前頁面的最新信息數據。
4. 根據最新信息進行更新操作
根據上一步服務器返回的數據,進行分支操作:如果是強制更新,則彈窗提示用戶需要強制刷新當前頁面才能繼續操作;如果是一般的非強制更新,則程序在后臺更新數據,用戶下次進入此頁面后更新生效;如果沒有更新,則不做任何操作,流程結束。
針對上述流程有一個補充:在APP啟動的時候向服務器請求預加載數據,提前對一些重要的RN資源進行加載,這樣在上述流程的第一步就可以直接利用RN緩存快速進入頁面,用戶體驗會更好。
結果分析
基于上述方案,可解決上述提到的三個問題:
-
在內置資源的時候,只需內置一分commonPart.jsbundle和相應入口頁面對應的diffPart.jsbundle。在通常情況下,commonPart.jsbundle占整個jsbundle近2/3的大小,這相比基于react-native-pushy而內置的資源節省了近2/3的大小。針對業務迭代過程的更新包,都只是業務代碼的更新,包的大小也得到了很好的控制。另外,由于RN的接口只能加載一個合并后的完整的jsbundle,但這個完整的jsbundle我們是實時合并的,是不存儲到文件系統的,只在內存中操作。通過實際運行,這種合并耗時時間很少,基本可忽略不計。這樣,即使app運行相對長的一段時間,也不會增加包的大小的。
-
服務器計算diff包的時候,由于只需對commonPart.jsbundle進行比較,所以計算增量的復雜度相比react-native-pushy降到了最低。APP端在合成diff的時候,只需要將一個common.jsbundle與一個diff.jsbundle進行合并即可,合并性能相比react-native-pushy平均要合并多個,得到了很大的提高。
-
本方案計算的更新,以某一個入口頁面對于的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組同學提供的雙平臺在加載時間上的性能測試結果(有緩存的情況下)。
上圖中的Web頁面加載時間是在網絡狀況良好的情況下的數據。從圖中可以看出,RN頁面基本上是秒進,這讓從點擊發布按鈕到展示發布頁面期間的用戶流失基本降到了最低。
經過項目組成員的辛苦努力,React Native在58 App上算是邁出了第一步,官方SDK現在是一個星期一個版本的更新節奏,后期開發中肯定還會有好多坑,躍坑的過程肯定很精彩!
來自:http://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667376230&idx=1&sn=77f66b21ed1a8bf75f8e49b4327fa0dc&chksm=84f33f28b384b63e0d801aa7f2d11314dff105aa82122d043cfc552e52f2877e6c6d07ca016d&scene=0