獲得 APP 是一個三年多的產品,最初採用純 Native 的方式開發,在 18 年初,咱們開始了 Hybyid 開發技術方案的探索和實踐, 目前獲得 APP 共包含了 ReactNative 和 Webview 兩套 Hybrid 方案。本文從時間維度上,重點回顧一下 Webview Hybrid 方案在獲得 APP 從 0 到 1 的過程,也但願咱們的經歷能夠給一些想落地 Hybrid 方案的團隊一點啓發。前端
獲得是一個重運營場景的產品,APP 內大部分的功能都會有分享功能。18 年初時,開發一個功能,基本須要三端三我的。部分業務使用了內嵌 Webview 、類瀏覽器式的方案,雖然知足了跨端,但體驗較差。因此最初的目的是但願有一套跨平臺方案,一套代碼能夠三端執行,而且有較好的體驗,這是當時 Hybrid 的架構圖:vue
除了 Webview,當時較爲流行的跨平臺方案主要是 ReactNative、Weex,對比了兩個方案,Weex 較爲接近咱們團隊的技術棧,而 RN 當時社區較爲成熟,最終咱們認爲社區更重要一些,因此選擇了 RN。git
在 RN 調研階段,咱們發現 RN 雖然支持三端和動態更新,可是須要配套的基礎設施才能夠實現其動態更新的能力,所以咱們須要一個離線資源的管理系統,可以動態更新客戶端內部的 RN 文件,而咱們在思考和設計這個離線資源管理系統時,發現一樣的思路能夠應用於 Webview,咱們能夠把前端代碼打成離線包,經過離線資源管理系統進行更新,而 Weview 在啓動過程當中,僅須要訪問數據 API 而不須要下載 HTML/JS/CSS 等,也算是變相的增長了離線能力。github
所以,咱們制定了最初的 Roadmap:web
作一個技術驅動的項目就像是作一個產品,須要先梳理清楚需求、使用場景等,再想思考技術架構和實現細節。咱們首先爲項目起了個名字,叫 Seeder。(爲何起這個名字,其實沒什麼意義,主要是內部沒有其餘系統叫 Seeder。。。)算法
經過梳理,咱們認爲 Seeder 須要達成如下目標:vue-router
明確目標後,咱們要作技術選型和架構,在技術選型上,咱們使用團隊熟悉的 Nodejs+Mongodb 組合,架構圖以下:json
服務端包含 Seeder 和 CDN 兩部分,CDN 部分主要是用來承接資源包的下載。Seeder 則拆分爲 Updater 服務和 Manager 服務:設計模式
經過合理的拆分,Updater 服務在咱們後續的壓力測試中,2 臺 8C16G 機器能夠穩定承載 6000QPS;數組
既然是對資源包進行管理,咱們須要定義資源包的格式和約束。
格式方面,咱們選擇了 tgz 格式,即便用 tar 進行歸檔,用 gzip 進行壓縮的格式,以減小傳輸體積。
文件結構方面,在原有資源目錄結構下的根目錄,增長了一個 info.json 格式的文件,用來描述包的信息。
咱們來看下一 package.json 的結構:
appId:標示包的應用 ID;
version:標示這個包的版本;
depend.containerVersion: 標示這個包依賴的容器版本,目前的容器只有客戶端;
files: 一個數組,記錄全部的文件和路徑及其 MD5;
meta: 擴展信息字段,這裏使用了兩個擴展字段,後面詳細講這兩個字段
增量更新指的是咱們只須要下載一個 Patch 包,安裝 Patch 包以後便可以完成應用的更新,像咱們經常使用的 VSCode 之類的軟件、大部分手機遊戲,都支持增量更新。實現增量更新關鍵點是增量算法,經過調研,最終選擇了支持二進制 diff 算法 bsdiff 。
確認算法以後就要開始思考增量包的實現方式,由於 bsdiff 是對單個二進制進行 diff,而咱們是一個包。所以有兩種方式:
下面咱們看下單文件 diff 的方案,先看一個增量包的結構:
相較於普通包,多了一個 update.json 文件,這個文件描述了整個包是若是變化的,基於這個文件和包內的其餘文件,即可以 Patch 到最早的版本,看一下 update.json 的結構:
files 是描述變化的文件。關鍵字段 type, 標示了變動類型,add、delete、move、modify 等,分別表示新增的、須要刪除的、發生目錄和文件名變化的、內容變化的文件。add、delete、move 只涉及到了文件的新增、刪除、變動路徑等操做,而 modify 則是用到了 bsdiff,表示這個文件發生變化,須要增量更新。
經過這種精細化的操做,能夠提升 patch 的效率,同時客戶端無需保留底包,基於解壓完的代碼文件就能夠完成增量更新。
梳理完了增量包結構,還有面臨一個問題,就是增量包的生成時機。一樣有兩種方案:
咱們最終採用了提早生成增量包的方案,由於包內容差別越大,增量帶來的收益越小,沒有必要生成全部版本的增量包。咱們在上傳包時,會同時生成歷史 10 個版本的增量包。基於咱們目前的更新頻率,10 個歷史版本目前基本能夠知足需求(後續不知足能夠調整,就是一個配置項)。固然,用戶長時間不打開 APP,可能再次打開,咱們已經更新了十幾個版本,這個時候只能經過全量包來進行更新。
看一下咱們調整後的架構變化:
完成了基礎設施的建設以後,客戶端的離線資源也具有的動態更新的能力,但普通的 Web 離線包還有如下的限制:
爲了解決以上問題,咱們決定開發一個應用層的框架。
咱們整個團隊最熟悉的技術棧是 Vue,所以 Adam 確定是基於 Vue 作封裝,在設計 Adam 以前,須要咱們先確認目標:
對目標進一步作分解:
咱們先看下一下 Adam 的總體架構,以便於咱們後續內容的表述:
每個 Web Package 就是一個應用,每一個 Application 實例對應一個 Global Store 和 vue-stack-router 的實例,對應多個 Page 實例。
每個頁面都由 Page Componets 和 Page Store 組成。其中 Page Store 的生命週期跟頁面保持一致。
最初的 Router 方案咱們是選了咱們經常使用的 vue-router,但在實現過程當中,遇到了如下問題:
爲了解決這些問題,咱們開發了 vue-stack-router (已開源,具體實現細節,感興趣的能夠直接看 github 代碼,內容較多,這裏不展開),相較於 vue-router,有如下新功能:
基於預渲染模式,咱們實現了手勢滑動返回的功能,即觸發手勢時,預渲染後一個頁面,此時同時存在兩個疊加在一塊兒的頁面,經過 JS 控制兩個頁面的動畫,即可以實習相似 Native 的滑動返回的效果。
提到狀態管理工具,共識都是簡單的項目無需使用 Store,複雜項目才能體現出 Store 的價值,其實無非是引入 Store 帶來了成本。咱們分析一下移動端頁面的特色:
所以,在移動端頁面,咱們追蹤狀態變化的收益可能不會很高,若是去掉狀態追蹤,Store 能夠變的很精簡, 看一下咱們本身精簡的 Store,原理以下:
class MyStore {
public name: string = '';
public updateName(name: string): void {
this.name = name;
}
}
const store = Vue.observable(new MyStore());
複製代碼
沒有狀態追蹤,只是最精簡的將狀態抽離到一個類中進行管理。
聊完了 Store 實現,再看看關於 Store 的組織形態,咱們經常使用 Vuex 和 Redux 都是單一組件樹,連 MobX 也有 mobx-state-tree 這種單一組件樹的社區方案。可是結合移動端業務的特色,單一組件樹會有些問題,對多頁面實例的支持,實現比較複雜。再一個,優秀的單一組件樹的組織一般是跟頁面分離的,通過單獨設計的,所以會帶來了額外的心智負擔。
基於以上死牢,最後咱們沒有采用單一組件樹,而是實現了多狀態的 Store 方案:一個頁面對應一個 Store,Store 和頁面的生命週期保持一致的方案。邏輯跟展示分離,頁面間又不耦合,最重要的是簡單;
數據緩存是體驗優化的一大利器,經過先渲染緩存數據,在更新正式數據的方式,咱們能夠馬上展示出一個頁面而無需等待。Adam 實現了三級緩存:
依次從路由數據、內存、LocalStorage 中取。路由數據是什麼呢,一般在客戶端內,頁面跳轉不少都是摘要信息跳往詳情信息的頁面(如列表的 item 跳詳情頁),其實前一個頁面已經包含一部分後續頁面的信息,這個時候能夠將前一個頁面的數據帶到後一個頁面中,後一個頁面即可以渲染出主要信息,提高用戶體驗。
那麼緩存的數據是哪裏來的呢,並不須要開發者手動寫入。咱們知道 View=fn(State),在 Store 方案中咱們已經將頁面的狀態都放到 store 中了,只須要緩存 Store 就能夠了。至於緩存和還原的時機,就是在頁面銷燬時,咱們序列化 Store,等頁面在打開,還原 Store 。
在開發 Adam 的同時,也不斷有同窗反饋,如今接入一個新的 Web Hybrid 業務比較麻煩,須要客戶端配置 webview,並且新業務依賴發版,是否是能夠咱們徹底不依賴客戶端呢?
答案是能夠的。
咱們在 Package 中增長了包的類型和包的全局路由信息,這樣客戶端在更新到包的信息時,能夠動態註冊路由,也就是全部的 Package 中的路由,都綁定到一個標準化的 webview,webview 啓動後,根據跳轉過來的路由加載對應 Package,已實現動態加載和註冊功能。
完成了 Adam 和 標準化容器後,咱們看一下最終接架構:
至此咱們能夠將每一個 Package 當作一個獨立的 Application 來更新和迭代。
功能方面,咱們接入了講座、電子書、評測、訓練營、獲得大學、活動系統、幫助中心等模塊,接入了 90+的頁面(其中 ReactNative 佔 30+,Web 佔 60+);
效率方面,咱們在一年半內支撐了 49 個功能模考動態更新了 1900 次。測試環境中,動態更新了 1.3 萬次;
性能方面,咱們從性能監控系統中找到兩個未使用和使用 Seeder 的功能進行對比(這個對比不太嚴謹,由於沒有同一個功能前後採用兩種方案的數據,咱們找了兩個功能相近,代碼量相近的兩個項目進行對比)。
普通 Webview 方案
Adam + Seeder 方案:
基本能夠看到,穩定性和效率都有較好的改善。
Hybrid 落地過程當中,咱們踩了不少坑,也有不少收貨,簡單談兩點。
第一個,如何評價一個技術方案的好壞?咱們有太多的標準:站在業務角度,是否是能知足需求及低成本的知足潛在的後續需求;站在運維角度,是否是帶來了新的部署運維成本。站在技術角度,咱們甚至能夠掏出一本設計模式大談一番。可是咱們不多有注意到技術方案的用戶體驗,這裏的用戶指的是使用你框架、庫的開發同窗。站在業務開發同窗的角度會發現,提供的方案確實解決了問題,可是使用這個方案過程當中,可能有 30% 工做是不屬於方案部分,可是屬於方案部分必須的,好比方案的入參是 A,開發者須要花大力氣才能獲得 A,才能使用這個方案。因此做爲框架、庫的開發者,要考慮清楚整個方案的使用場景,技術部分是否是能夠覆蓋整個場景,覆蓋不了要怎麼解決,是否須要提供自動化工具等等。
第二個,Hybrid 不是一個端的事情,而是三端一塊兒的事情,而做爲推進方,要儘量的瞭解三端,不瞭解能夠多跟各端同窗溝通交流,不要作成一方推進兩方配合,要讓你們感受是在一塊兒幹一件事情,這樣才能作好。
後續的規劃主要是有兩大方面: