由於我司的業務特性,需要 APP 能夠支持即時在無網路的場景下,也能夠正常使用 APP 的功能 那麼,為了讓一個用 web 前端實現的 APP 能夠在無網路的場景下,也能夠正常運行程式,這其中的離線方案就需要實現幾個關鍵點: 代碼的離線、更新 數據的下載、上傳、更新 本篇就想來講一講,我們在離線應用 ...
由於我司的業務特性,需要 APP 能夠支持即時在無網路的場景下,也能夠正常使用 APP 的功能
那麼,為了讓一個用 web 前端實現的 APP 能夠在無網路的場景下,也能夠正常運行程式,這其中的離線方案就需要實現幾個關鍵點:
- 代碼的離線、更新
- 數據的下載、上傳、更新
本篇就想來講一講,我們在離線應用方面的實現方案
代碼的離線和更新
web 應用不管是網頁還是 H5,通常都是線上服務,代碼都需要部署到線上伺服器
但是離線應用就不能只依賴於網路,在沒有網路的場景下,也需要想辦法讓用戶的客戶端可以獲取到程式代碼,這就需要依賴原生 APP 開發的能力了
我們採用的是 web + Cordova 的跨平臺 APP 方案:
Cordova 通過在原生 APP 上運行一個 WebView 來解析、運行 web 程式,而 web 程式通過 Cordova 插件來調用一些原生能力
WebView 載入 web 代碼時,支持載入本地的 web 文件,這裡的本地自然指的是用戶的移動設備
通過 APP 原生開發的能力,在打包 APP 時,就把 web 代碼打包進 APP 的安裝包(apk/ipa)中,就可以讓用戶安裝 APP 後,在本地拿到 web 代碼
就拿 Android 來說,可以讓 Android 開發的同事們打包時,把我們的 web 代碼打包進 assets 里,這是一個可以存放資源文件的目錄,打包時,裡面的文件不會被壓縮、編碼處理
打包結束後,Android 的 APP 是一個 apk 安裝包,可以把尾碼名改成 zip,然後解壓就可以看到我們的 web 文件都已經在安裝包內了(assets/www):
手機在安裝 APP 時,會將這些 web 代碼文件解壓到 APP 應用的私有目錄下,運行 APP 的時候,WebView 就可以載入用戶手機本地的 index.html,資源文件也都通過相對路徑載入,這樣就能達到將 web 代碼文件離線化、程式可直接不依賴於網路被載入運行
我們通過 inspect 來調試時,也可以看到載入的 web 資源文件的路徑地址,比如拿 Android 來說,WebView 通過 file 協議來載入存放在 APP 私有目錄里的本地 index.html 文件:
這樣一來,代碼的離線實現了,但也帶來了一個問題:代碼如何更新?也就是如何發版?
有兩種方案:
- 發佈新版本的 APP,將新 web 代碼文件打包進 APP 里
- 熱更新機制,先下載新的 web 代碼包,再藉助 Cordova 插件替換本地的 web 資源,最後重新載入 web 程式
兩種方案都可以達到更新 APP 的 web 代碼文件的效果,但有各自的優缺點,適用場景也不同:
-
第一種方案:由於需要依賴於原生 APP 的上架、更新機制,靈活性差、及時性低,通常是只有在 Cordova 插件變動,原生開發層面需要發版時才選擇這種方案
-
第二種方案:需要前端在程式里實現熱更新的檢測、下載機制,獲取到新的 web 代碼文件後,再藉助 Cordova 插件更新到本地,這種方案靈活性和及時性都很高,日常的業務迭代發版選擇這種方案即可
可以簡單稱呼兩種方案為:大版本更新和熱更新
其實,更準確來講,前端要自行實現的熱更新機制包括兩個方面:web 熱更新和 db 熱更新,前者是更新 web 代碼文件,後者是更新 db 資料庫表
db 的熱更新可以放到下個講數據的章節時再介紹,這裡先來講講 web 熱更新,實現這個機制的關鍵點有兩點:檢測和下載更新
既然是檢測,自然是需要有比較,所以需要有一套版本管理機制,來進行用戶本地版本和伺服器最新版本間的比較
版本管理機制可以包含很多方面的檢測,其中比較重要的是代碼版本的比較
來看一個抓包請求:
常用的檢測邏輯是比對版本,而版本是由 APP 大版本(如:6.5.0)拼接上 web 熱更新版本(如:00)組成,前者可以通過 Cordova 插件獲取,後者則需要自行存儲維護,可以簡單存儲在 localstorage 里即可
當檢測到需要進行 web 熱更新了,這時候介面會下發新版本的 web 代碼文件的下載地址,我們就可以藉助 Cordova 插件去下載並更新到本地了
藉助 Cordova 插件,我們可以調用原生的下載能力去下載 web 代碼文件,下載完成後,再通過 Cordova 插件,將代碼更新包解壓到 APP 應用的存放 web 代碼文件的目錄里
因為下載是調用的原生 APP 的下載能力,這個數據包的請求在 inspect 調試里是抓不到包的,如果想抓取下載的更新包,可以走代理抓包(推薦使用 whistle)
另外,解壓操作預設都是同名替換原則,這意味著不會刪除歷史文件,那麼藉助這個特性,我們甚至可以做到增量更新,只發佈本次迭代有改動的幾個文件即可
如果你的手機有 root 許可權,那麼可以自行到 APP 的私有目錄里(data/data/{包名})查看下:
這就是 web 熱更新的大體思路
上圖的 AppCloud 平臺是我司內部研發的專門用來給前端開發人員直接通過平臺操作完成打包行為的項目
當然,web 熱更新還有很多細節方面,比如在什麼時機發起更新檢測、本地開髮結束後如何打包並壓縮上傳到專門的文件伺服器等等。這些方面通常是根據不同項目、不同團隊規範進行制定,就不展開了
這樣一來,代碼的離線化確保了 APP 運行時,代碼文件早已在用戶本地設備上,自然就可以支持離線運行
而 web 熱更新機制,又確保了用戶設備能夠及時更新到最新版的 web 代碼文件
兩者結合就解決 web 代碼的部署、發版問題,而這僅僅是讓 web 程式能夠在用戶設備上離線運行的基礎,程式運行後,交互以及業務功能依賴的數據處理也是應用能否支持離線運行的關鍵,所以下麵繼續來看看數據方面的處理
數據的下載、上傳和更新
上一節講了代碼的離線化和熱更新機制,這是確保 APP 能夠實現離線應用的基礎,在此基礎上,各個業務組再去開發實現各自的業務離線功能
而業務功能的正常運行則是由頁面數據支撐,所以離線的關鍵還是在於對各種數據的處理:
- 頁面數據的離線化
- 用戶本地離線數據的上傳
- 服務端新數據的下載更新
簡單概括來講的話,也就是:離線數據的下載、上傳和更新機制
下麵就一條條的來展開分析,首先看第一個
頁面數據的離線化
所謂的離線化,其實也就是存儲,頁面渲染的數據也從本地取的話,那頁面的交互自然就不依賴於網路了
那麼,問題來了:
- 要存哪裡?
- 存什麼數據?
存自然就是要持久化存儲,但對於前端來說,在持久化存儲方面的能力並不是很強,localstorage 有大小限制;indexedDB 是非關係型資料庫,且存在相容性問題,所以我們採用的是通過 Cordova 插件調用原生能力來存儲在 sqlite 的 db 文件中
sqlite 是原生 APP 支持的資料庫存儲方案,語法方面跟 mysql 基本一樣,存儲後就是一份 db 文件,調試時可直接通過 Cordova 插件執行 sql 查詢數據,或者將 db 文件拿到 PC 電腦上藉助諸如 Navicat 等資料庫軟體快速查看,甚至如果手機有 root 許可權,可直接通過 sqlite3 命令來調試:
存哪裡解決了,接下去就是思考要存的是什麼數據?
正常來說,對於線上的 web 應用,前端的界面數據大多是實時來自 API 介面返回的 json 格式數據,所以如果是從介面數據緩存角度思考的話,那麼也可以解決界面的呈現問題,但也僅僅只支持這種純粹用來展示的界面場景,比如待辦頁、工作台等這類只展示不依賴業務數據的界面場景
對於這些介面數據緩存的場景,存儲的地方也可以簡單存儲到 localstorage 里,也可以直接上手體驗 ServiceWorker 新特性
但是還有部分界面會跟隨用戶操作產生的各種數據進行變化,界面並不單純展示,也有業務交互,在這些場景下,介面數據緩存的方案是不可行的
對於這類場景,換種角度思考,既然前面選擇了使用 sqlite 這種關係型資料庫來存儲數據,那麼其實 APP 本身就有了一個類似後端的角色職責,那要存的數據,直接就類似後端資料庫一樣存儲各種業務表即可,而表裡存的自然也就是一些原始數據了
需要註意的是,在這類離線應用里的一些 API 用途跟正常的 web 線上應用就有些不大一樣了,這些 API 可以看成純粹是用來同步後端資料庫表和 APP 本地表數據的角色,介面返回的數據大多也都是 sql 語句,真實數據是需要執行完 sql 才會插入到 db
所以,對於這類離線應用 APP,其本身架構上就會比較複雜一些,畢竟除了視圖層外,還需要實現從 db 里取出原始數據,然後經過一系列業務邏輯處理,將數據進行加工、轉換成界面所需的數據結構。因為這中間基本沒有 API 的參與了,那麼原先一些後端角色需要處理的職責就需要 APP 本身自行承擔了
簡單來小結下,頁面數據的離線化需要根據不同場景來進行不同的處理:
- 對於一些只展示的頁面,比如待辦頁、工作台,可直接存儲介面數據到 localstorage 里,有網時走介面,無網時走緩存即可
- 對於支持用戶進行交互的頁面,需要提前下載好各個業務表數據到 sqlite 的 db 中,不管有網無網,都只從 db 里取數和存數,再自行處理業務邏輯後丟給界面
用戶本地離線數據的上傳
上面說到,可交互產生數據的界面,不管有網無網,都只走本地 db 的取數和存數處理,這也就意味著,隨著用戶的不斷操作,本地 db 里的數據就會發生變化
那麼,用戶操作產生的這些離線數據,就必需得想辦法上傳給服務端,對於這一點,也有幾個方面需要考慮清楚:
- 什麼時候上傳?
- 上傳什麼數據?
上傳的時機通常有自動上傳和手動上傳兩種,自動上傳需要考慮的場景比較多,比如有網無網、數據有效無效等等,所以實現上更多的還是傾向於交給用戶手動上傳
而手動上傳就需要有一個用戶交互的時機來觸發上傳,這需要依賴不同業務特性來進行設計
那麼,上傳的時候,該上傳的是什麼數據呢?
用戶操作產生的數據肯定會插入或更新到本地的數據表中,這樣就跟原生數據摻雜在一起了,而上報的數據肯定不會是全量數據,所以本地得有一種機制來記錄用戶操作過程中產生的增量數據
所以資料庫中,除了一些業務表之外,還會有一張增量表:
表結構主要有 type、operation、releation_id 幾個欄位,當然還有其他欄位,但這些欄位基本是必須的,覆蓋了大多數場景的使用
type 和 operation 欄位用來標誌這次操作產生了哪種類型數據的變化,比如 type=add
,operation=問題
,表示新登記了一個問題,而跟問題相關的業務表比如 xxx_problem 表, xxx_problem_images 等表裡就會新增問題相關數據,releation_id 就會關聯著這些業務表裡新增的數據 id,這樣就可以根據增量表到各個業務表裡取出增量的數據
當增量數據上傳完畢後,本地則需要將這些增量數據從增量表中移除,這樣才不會重覆上傳
這是基本的離線數據的上傳處理,當然,還有很多細節方面的處理,比如性能方面:如何進行分批上傳,如何進行後臺隊列上傳等等
再比如數據處理方面:圖片文件數據得先上傳文件伺服器,再回填 url,最後再 sql 取數組裝成介面需要的格式上傳
再比如上傳過程中,在各個步驟失敗時的場景處理和提示等
這些都依賴於產品業務特性來進行設計實現即可
服務端新數據的下載更新
數據有上傳自然也有下載,下載分兩種場景,一是本地無數據時的初始化下載場景,二是同步更新服務端數據的場景
針對這一點,同樣也需要考慮清楚:
-
更新什麼數據?
-
什麼時候更新數據?
首先需要清楚,都有什麼數據是需要下載更新的,如果從離線化存儲的資料庫 db 角度來看的話,其實可以分成兩類:表結構數據和表數據
db 熱更新
因為頁面數據基本都存在本地的資料庫表中了,既然是資料庫,那麼就會有建庫、庫版本管理的場景存在
所以,需要有一套資料庫版本管理的機制,也俗稱 db 熱更新
建庫時,最好根據用戶來建資料庫的,不同用戶有不同的資料庫,這樣就能確保在 APP 內切換用戶使用時,相互間互不影響
當在本地沒有找到用戶的資料庫時,自然就是先建資料庫。程式里會維護一份 init.sql 文件,裡面是各個表最新結構的建表 sql,建庫時,就會執行這份 init.sql 來建立資料庫。當本地已經有資料庫存在時,就需要檢測是否需要 db 熱更新
db 熱更新有兩種思路:一種是走後端維護的資料庫升級 sql,一種是純前端維護的資料庫升級 sql
如果是走後端維護的資料庫升級 sql 的話,就需要有檢測機制,跟 web 熱更新類似,需要有版本的比較。資料庫中會創建一張 _version 表,這張表裡會記錄上次資料庫升級時的 web 版本,然後在每次 web 熱更新結束後會去比對,如果版本不一致時,就向後端獲取兩個版本之間的 db 升級 sql:
如果是走純前端維護資料庫升級 sql 的話,那麼前端里除了維護一份最新的資料庫升級 sql 之外,還需要自行判斷是否需要進行資料庫升級檢測,可以是簡單的判斷是否有該欄位或該表存在的思路,也可以自己搞個版本檢測機制
數據下載和更新
表結構數據的下載和更新解決了,那麼就能來服務表數據的下載和更新了,對於表數據,我們也需要清楚它的一些分類
從我司業務來看的話,表數據大概可以分為兩類:基礎數據和業務數據
基礎數據可以理解成:所有用戶都一樣,下載到本地基本就不會發生變化的數據
業務數據則就是:隨著不同用戶的操作,會在原有數據基礎上不斷新增、變化的數據
不同類型的數據下載、更新的時機和方式也不同
對於用戶首次安裝使用 APP,是需要先進行數據的下載才能正常使用 APP 功能,因為頁面數據都是依賴於本地的 db 資料庫
那麼,對於業務無關的基礎數據自然需要先進行下載,通常是打開 APP 後進入首頁就會檢測下載,然後再讓用戶手動選擇指定業務數據下載即可
這是數據下載的處理,不管是基礎數據還是業務數據,準則都是初次使用本地無此數據時就需要下載,區別在於基礎數據是程式自動檢測進行下載,而業務數據通常是由用戶手動觸發進行
這麼處理是因為,業務數據量通常都是非常大,而用戶當前所開展的業務可以需要的僅僅是某部分業務,那麼交由用戶自行決定下載那部分業務數據,可以避免下載大量無關數據而造成用戶體驗不好
數據的下載場景比較簡單,需要特別註意處理的是數據的更新,既然要更新,就需要有檢測更新的機制
檢測機制則是通過時間戳和版本號實現,時間戳機制可以讓服務端知道用戶本地當前數據是否需要更新,而版本號則是讓服務端進行數據相容的處理,因為有可能存在不同用戶使用不同版本的 APP,表結構也有可能是不同的,所以還需要根據版本進行控制
當有數據需要更新時,服務端就可以將新數據進行下發,這時候也分兩種場景,是全量更新還是增量更新,對於業務數據來說,通常都是增量更新,因為量可能會很大;基礎數據可以看場景選擇使用全量更新,一來實現簡單,二來有的數據量並不大
但有一點需要註意,對於業務數據來說,如果用戶本地有離線數據,那麼只能是等用戶上傳結束才能觸發數據的更新,這是因為,業務數據會存在大家同時修改同一份數據的場景,這就會造成數據衝突,而數據衝突一旦沒處理好,很可能就會導致用戶本地的離線數據丟失,因為可能被覆蓋,也可能鍵值衝突導致程式異常
對於這種場景,最好辦法就是讓用戶先進行數據的上傳,由服務端來根據業務場景解決衝突並備份,這才能儘可能降低用戶數據丟失的問題出現
總結
APP 的離線方案其實就是要把 APP 運行期間所有數據都離線化,只是這數據涉及到了頁面代碼和頁面數據,頁面代碼的離線化和更新機制可藉助 Cordova 插件實現,原理其實就是將 web 代碼文件下載存儲到用戶本地設備,然後 WebView 載入本地資源,再結合 web 熱更新機制即可
頁面數據的離線化則需要各個業務組根據各自的業務場景來決定離線化的數據有哪些,下載數據的時機是什麼、存儲的位置是什麼,存儲的是什麼數據,離線數據如何進行上傳,上傳時機是什麼,上傳的是什麼數據,數據的更新時機,如何更新,更新什麼數據等等
不同業務需求場景,數據的下載、上傳和更新在具體實現上會有些差異,但涉及到的知識點無外乎就是:sqlite、增量表、時機戳等等