seed加速器安卓安裝「seeder系統加速」

得到 APP 是一個三年多的產品,最初採用純 Native 的方式開發,在 18 年初,我們開始了 Hybyid 開發技術方案的探索和實踐, 目前得到 APP 共包含了 ReactNative 和 Webview 兩套 Hybrid 方案。本文從時間維度上,重點回顧一下 Webview Hybrid 方案在得到 APP 從 0 到 1 的過程,也希望我們的經歷可以給一些想落地 Hybrid 方案的團隊一點啟發。

1. 背景和動機

得到是一個重運營場景的產品,APP 內大部分的功能都會有分享功能。18 年初時,開發一個功能,基本需要三端三個人。部分業務使用了內嵌 Webview 、類瀏覽器式的方案,雖然滿足了跨端,但體驗較差。所以最初的目的是希望有一套跨平台方案,一套代碼可以三端執行,並且有較好的體驗,這是當時 Hybrid 的架構圖:

得到 hybrid 架構的演進之路

除了 Webview,當時較為流行的跨平台方案主要是 ReactNative、Weex,對比了兩個方案,Weex 較為接近我們團隊的技術棧,而 RN 當時社區較為成熟,最終我們認為社區更重要一些,所以選擇了 RN。

在 RN 調研階段,我們發現 RN 雖然支持三端和動態更新,但是需要配套的基礎設施才可以實現其動態更新的能力,因此我們需要一個離線資源的管理系統,能夠動態更新客戶端內部的 RN 文件,而我們在思考和設計這個離線資源管理系統時,發現同樣的思路可以應用於 Webview,我們可以把前端代碼打成離線包,通過離線資源管理系統進行更新,而 Weview 在啟動過程中,僅需要訪問數據 API 而不需要下載 HTML/JS/CSS 等,也算是變相的增加了離線能力。

因此,我們制定了最初的 Roadmap:

  1. 先開發離線資源管理系統;
  2. 完成之後接入 Web 離線包,因為 Web 離線包開發成本較低,可以快速的改善現有項目的體驗,快速收益;
  3. 最後在進行 RN 的開發和接入;

2. 離線資源包管理系統-Seeder

做一個技術驅動的項目就像是做一個產品,需要先梳理清楚需求、使用場景等,再想思考技術架構和實現細節。我們首先為項目起了個名字,叫 Seeder。(為什麼起這個名字,其實沒什麼意義,主要是內部沒有其他系統叫 Seeder。。。)

2.1. 目標

通過梳理,我們認為 Seeder 需要達成以下目標:

  1. 可以動態更新資源;
  2. 可以支持非最新版客戶端進行更新;
  3. 支持增量更新;
  4. 支持多頻道發佈;

2.2. 技術選型和架構

明確目標後,我們要做技術選型和架構,在技術選型上,我們使用團隊熟悉的 Nodejs+Mongodb 組合,架構圖如下:

得到 hybrid 架構的演進之路

服務端包含 Seeder 和 CDN 兩部分,CDN 部分主要是用來承接資源包的下載。Seeder 則拆分為 Updater 服務和 Manager 服務:

  1. Updater 服務:主要是承接處理客戶端的更新請求;
  2. Manager 服務:主要進行資源包及相關配置的管理,包括生成 diff 包等等;

通過合理的拆分,Updater 服務在我們後續的壓力測試中,2 台 8C16G 機器可以穩定承載 6000QPS;

2.3. 關鍵實現點 – Package 定義

既然是對資源包進行管理,我們需要定義資源包的格式和約束。

格式方面,我們選擇了 tgz 格式,即使用 tar 進行歸檔,用 gzip 進行壓縮的格式,以減少傳輸體積。

文件結構方面,在原有資源目錄結構下的根目錄,增加了一個 info.json 格式的文件,用來描述包的信息。

![Package 結構(https://piccdn.luojilab.com/fe-oss/default/image-20200114171319102.png)

我們來看下一 package.json 的結構:

得到 hybrid 架構的演進之路
  1. appId:標示包的應用 ID;
  2. version:標示這個包的版本;
  3. depend.containerVersion: 標示這個包依賴的容器版本,目前的容器只有客戶端;
  4. files: 一個數組,記錄所有的文件和路徑及其 MD5;
  5. meta: 擴展信息字段,這裡使用了兩個擴展字段,後面詳細講這兩個字段
    1. type:包的類型
    2. routes:包需要註冊的路由列表

2.4. 關鍵實現點 – 增量更新

增量更新指的是我們只需要下載一個 Patch 包,安裝 Patch 包之後即可以完成應用的更新,像我們常用的 VSCode 之類的軟件、大部分手機遊戲,都支持增量更新。實現增量更新關鍵點是增量算法,通過調研,最終選擇了支持二進制 diff 算法 bsdiff 。

確認算法之後就要開始思考增量包的實現方式,因為 bsdiff 是對單個二進制進行 diff,而我們是一個包。因此有兩種方式:

  1. 基於歸檔壓縮完的 tgz 包進行 diff 和 patch,這種方案的優勢是實現成本低,帶來的問題是客戶端必須保留一份底包,並且由 於 Package 在客戶端是下載完先解壓才能執行,這種方案無法連續 patch 升級(不能增量從 v1.0->v1.1->v1.2,只能 v1.0->v1.1,v1.0->v1.2);
  2. 基於單文件 diff,即增量包其實包含多個 patch 文件,包含了描述 Package 變更信息的文件,這種方案雖然實現會複雜些,但是並沒有方案 1 的各種問題,因此我們採用了是單文件 diff 的方案;

下面我們看下單文件 diff 的方案,先看一個增量包的結構:

得到 hybrid 架構的演進之路

相較於普通包,多了一個 update.json 文件,這個文件描述了整個包是如果變化的,基於這個文件和包內的其他文件,便可以 Patch 到最先的版本,看一下 update.json 的結構:

得到 hybrid 架構的演進之路

files 是描述變化的文件。關鍵字段 type, 標示了變更類型,add、delete、move、modify 等,分別表示新增的、需要刪除的、發生目錄和文件名變化的、內容變化的文件。add、delete、move 只涉及到了文件的新增、刪除、變更路徑等操作,而 modify 則是用到了 bsdiff,表示這個文件發生變化,需要增量更新。

通過這種精細化的操作,可以提高 patch 的效率,同時客戶端無需保留底包,基於解壓完的代碼文件就可以完成增量更新。

梳理完了增量包結構,還有面臨一個問題,就是增量包的生成時機。同樣有兩種方案:

  1. 請求來了生成增量包,好處就是一定會有增量包,問題是增量包的生成是一個 CPU 密集型操作,無法支持高並發;
  2. 提前生成增量包,但是只能提前生成指定版本數量的增量包,但可能存在較老版本沒有增量包可用;

我們最終採用了提前生成增量包的方案,因為包內容差異越大,增量帶來的收益越小,沒有必要生成所有版本的增量包。我們在上傳包時,會同時生成歷史 10 個版本的增量包。基於我們目前的更新頻率,10 個歷史版本目前基本可以滿足需求(後續不滿足可以調整,就是一個配置項)。當然,用戶長時間不打開 APP,可能再次打開,我們已經更新了十幾個版本,這個時候只能通過全量包來進行更新。

2.5. 架構變化

看一下我們調整後的架構變化:

得到 hybrid 架構的演進之路

3. 應用框架 – Adam

完成了基礎設施的建設之後,客戶端的離線資源也具備的動態更新的能力,但普通的 Web 離線包還有以下的限制:

  1. 每個 Webview 只能有一個頁面,無法實現複雜的功能(為了跟客戶端保持一致的頁面交互體驗,每個 Webview 只有一個頁面,這樣前進後退、導航條的表現是一致的);
  2. 無法控制導航條,一些需要定製導航條的功能依賴客戶端;
  3. 沒有體系化的框架,無法統一處理異常、緩存等;

為了解決以上問題,我們決定開發一個應用層的框架。

3.1. 目標和分解

我們整個團隊最熟悉的技術棧是 Vue,因此 Adam 肯定是基於 Vue 做封裝,在設計 Adam 之前,需要我們先確認目標:

  1. 功能上:一個 Package 可以作為一個完整的 Application,能夠完整地實現一個功能模塊,包括多頁面的功能等;
  2. 技術上:實現標準化的解決方案,由框架處理緩存、異常頁面等通用邏輯;

對目標進一步做分解:

  1. 需要客戶端將界面全部交給 Webview 處理;
  2. 需要 Router,並且像客戶端一樣,支持棧式管理頁面的路由;
  3. 頁面要實現客戶端相同的前進和後退動效,要支持滑動返回上一個頁面;
  4. 需要抽象緩存和異常頁面等到框架層;
得到 hybrid 架構的演進之路

3.2. 架構圖

我們先看下一下 Adam 的整體架構,以便於我們後續內容的表述:

得到 hybrid 架構的演進之路

每一個 Web Package 就是一個應用,每個 Application 實例對應一個 Global Store 和 vue-stack-router 的實例,對應多個 Page 實例。

每一個頁面都由 Page Componets 和 Page Store 組成。其中 Page Store 的生命周期跟頁面保持一致。

3.3. 關鍵實現點 – Router

最初的 Router 方案我們是選了我們常用的 vue-router,但在實現過程中,遇到了以下問題:

  1. 實現類似棧式的路由較為困難。客戶端內的頁面大部分都具有棧式的特點,頁面實例的存活取決於是否在棧中。而 vue-router 中,組件實例的存活則是取決與是否使用了 kee-alive 組件;
  2. 實現兩個頁面間的、類似 Native 的滑動返回較為困難;
  3. 無法實現多例頁面。Native 中,A 頁面跳轉 A 頁面,會產生一個新的 A 頁面的實例。vue-router 中,A 頁面跳轉 A 頁面會重新渲染現有的 A 頁面,也就是 A 頁面始終是單例的;

為了解決這些問題,我們開發了 vue-stack-router (已開源,具體實現細節,感興趣的可以直接看 github 代碼,內容較多,這裡不展開),相較於 vue-router,有以下新功能:

  1. 棧式的路由管理;
  2. 路由間數據傳遞;
  3. 支持更細粒度、可定製的路由過渡效果;
  4. 支持預渲染;

基於預渲染模式,我們實現了手勢滑動返回的功能,即觸發手勢時,預渲染後一個頁面,此時同時存在兩個疊加在一起的頁面,通過 JS 控制兩個頁面的動畫,便可以實習類似 Native 的滑動返回的效果。

3.4. 關鍵實現點 – Store

提到狀態管理工具,共識都是簡單的項目無需使用 Store,複雜項目才能體現出 Store 的價值,其實無非是引入 Store 帶來了成本。我們分析一下移動端頁面的特點:

  1. 展示為主
  2. 頁面間耦合性低
  3. 數據流簡單

因此,在移動端頁面,我們追蹤狀態變化的收益可能不會很高,如果去掉狀態追蹤,Store 可以變的很精簡, 看一下我們自己精簡的 Store,原理如下:

classMyStore{
public name:string='';
public updateName(name:string):void{
this.name = name;
}
}
const store =Vue.observable(newMyStore());

沒有狀態追蹤,只是最精簡的將狀態抽離到一個類中進行管理。

聊完了 Store 實現,再看看關於 Store 的組織形態,我們常用 Vuex 和 Redux 都是單一組件樹,連 MobX 也有 mobx-state-tree 這種單一組件樹的社區方案。但是結合移動端業務的特點,單一組件樹會有些問題,對多頁面實例的支持,實現比較複雜。再一個,優秀的單一組件樹的組織通常是跟頁面分離的,經過單獨設計的,因此會帶來了額外的心智負擔。

基於以上死牢,最後我們沒有採用單一組件樹,而是實現了多狀態的 Store 方案:一個頁面對應一個 Store,Store 和頁面的生命周期保持一致的方案。邏輯跟展現分離,頁面間又不耦合,最重要的是簡單;

3.5. 緩存

數據緩存是體驗優化的一大利器,通過先渲染緩存數據,在更新正式數據的方式,我們可以立刻展現出一個頁面而無需等待。Adam 實現了三級緩存:

得到 hybrid 架構的演進之路

依次從路由數據、內存、LocalStorage 中取。路由數據是什麼呢,通常在客戶端內,頁面跳轉很多都是摘要信息跳往詳情信息的頁面(如列表的 item 跳詳情頁),其實前一個頁面已經包含一部分後續頁面的信息,這個時候可以將前一個頁面的數據帶到後一個頁面中,後一個頁面便可以渲染出主要信息,提升用戶體驗。

那麼緩存的數據是哪裡來的呢,並不需要開發者手動寫入。我們知道 View=fn(State),在 Store 方案中我們已經將頁面的狀態都放到 store 中了,只需要緩存 Store 就可以了。至於緩存和還原的時機,就是在頁面銷毀時,我們序列化 Store,等頁面在打開,還原 Store 。

4. 標準化容器

在開發 Adam 的同時,也不斷有同學反饋,現在接入一個新的 Web Hybrid 業務比較麻煩,需要客戶端配置 webview,而且新業務依賴發版,是不是可以我們完全不依賴客戶端呢?

答案是可以的。

4.1. 路由協議

我們在 Package 中增加了包的類型和包的全局路由信息,這樣客戶端在更新到包的信息時,可以動態註冊路由,也就是所有的 Package 中的路由,都綁定到一個標準化的 webview,webview 啟動後,根據跳轉過來的路由加載對應 Package,已實現動態加載和註冊功能。

4.2. 最終架構

完成了 Adam 和 標準化容器後,我們看一下最終接架構:

得到 hybrid 架構的演進之路

至此我們可以將每個 Package 當做一個獨立的 Application 來更新和迭代。

5. 總結和思考

5.1. 成果

功能方面,我們接入了講座、電子書、評測、訓練營、得到大學、活動系統、幫助中心等模塊,接入了 90+的頁面(其中 ReactNative 占 30+,Web 占 60+);

得到 hybrid 架構的演進之路

效率方面,我們在一年半內支撐了 49 個功能模塊動態更新了 1900 次。測試環境中,動態更新了 1.3 萬次;

性能方面,我們從性能監控系統中找到兩個未使用和使用 Seeder 的功能進行對比(這個對比不太嚴謹,因為沒有同一個功能先後採用兩種方案的數據,我們找了兩個功能相近,代碼量相近的兩個項目進行對比)。

普通 Webview 方案

得到 hybrid 架構的演進之路

Adam + Seeder 方案:

得到 hybrid 架構的演進之路

基本可以看到,穩定性和效率都有較好的改善。

5.2. 思考

Hybrid 落地過程中,我們踩了很多坑,也有很多收貨,簡單談兩點。

第一個,如何評價一個技術方案的好壞?我們有太多的標準:站在業務角度,是不是能滿足需求及低成本的滿足潛在的後續需求;站在運維角度,是不是帶來了新的部署運維成本。站在技術角度,我們甚至可以掏出一本設計模式大談一番。但是我們很少有注意到技術方案的用戶體驗,這裡的用戶指的是使用你框架、庫的開發同學。站在業務開發同學的角度會發現,提供的方案確實解決了問題,但是使用這個方案過程中,可能有 30% 工作是不屬於方案部分,但是屬於方案部分必須的,比如方案的入參是 A,開發者需要花大力氣才能得到 A,才能使用這個方案。所以作為框架、庫的開發者,要考慮清楚整個方案的使用場景,技術部分是不是可以覆蓋整個場景,覆蓋不了要怎麼解決,是否需要提供自動化工具等等。

第二個,Hybrid 不是一個端的事情,而是三端一起的事情,而作為推動方,要儘可能的了解三端,不了解可以多跟各端同學溝通交流,不要做成一方推動兩方配合,要讓大家感覺是在一起干一件事情,這樣才能做好。

5.3 後續的規劃

後續的規劃主要是有兩大方面:

  1. Adam 的多環境多端的支持,覆蓋得到業務「端」的場景;
  2. Seeder 更加靈活的更新場景,比如支持 Lazy 加載等;

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/269000.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-16 13:13
下一篇 2024-12-16 13:13

相關推薦

發表回復

登錄後才能評論