首頁>技術>

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

1. 背景和動機

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

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

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

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

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

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

2.1. 目標

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

可以動態更新資源;可以支援非最新版客戶端進行更新;支援增量更新;支援多頻道釋出;2.2. 技術選型和架構

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

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

Updater 服務:主要是承接處理客戶端的更新請求;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 的結構:

appId:標示包的應用 ID;version:標示這個包的版本;depend.containerVersion: 標示這個包依賴的容器版本,目前的容器只有客戶端;files: 一個數組,記錄所有的檔案和路徑及其 MD5;meta: 擴充套件資訊欄位,這裡使用了兩個擴充套件欄位,後面詳細講這兩個欄位type:包的型別routes:包需要註冊的路由列表2.4. 關鍵實現點 - 增量更新

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

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

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

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

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

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

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

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

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

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

2.5. 架構變化

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

3. 應用框架 - Adam

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

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

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

3.1. 目標和分解

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

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

對目標進一步做分解:

需要客戶端將介面全部交給 Webview 處理;需要 Router,並且像客戶端一樣,支援棧式管理頁面的路由;頁面要實現客戶端相同的前進和後退動效,要支援滑動返回上一個頁面;需要抽象快取和異常頁面等到框架層;3.2. 架構圖

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

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

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

3.3. 關鍵實現點 - Router

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

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

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

棧式的路由管理;路由間資料傳遞;支援更細粒度、可定製的路由過渡效果;支援預渲染;

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

3.4. 關鍵實現點 - Store

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

展示為主頁面間耦合性低資料流簡單

因此,在移動端頁面,我們追蹤狀態變化的收益可能不會很高,如果去掉狀態追蹤,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 實現了三級快取:

依次從路由資料、記憶體、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 和 標準化容器後,我們看一下最終接架構:

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

5. 總結和思考5.1. 成果

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

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

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

普通 Webview 方案

Adam + Seeder 方案:

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

5.2. 思考

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

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

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

5.3 後續的規劃

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

Adam 的多環境多端的支援,覆蓋得到業務“端”的場景;Seeder 更加靈活的更新場景,比如支援 Lazy 載入等;

10
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • SQLServer幾種操作方法