引言
上一篇原理篇,我們已經詳細地闡述了 Hybrid App 的基礎原理,瞭解了 Native端 和 H5端 是如何通訊的,還有 bridge 的設計和接入。而本篇文章將開始把這些原因進一步實踐,用程式碼真正地去實現一套完整且穩定的 Hybrid 方案。如果對原理還有疑問的小夥伴,請移步Hybrid App技術解析 — 原理篇,只有在理解了理論的基礎上,進一步與實踐相結合,才能真正地去深入一項技術。
如果大家有什麼更好的方案或建議,可以到 github.com/xd-tayde 上與我進行討論哈!
摩天大樓
說了那麼一大堆理論知識,可能有小夥伴會說:“ 你是不是吹流弊啊。”。。那就先來簡單介紹下我們已經使用這套方案落地的專案之一。
這是一個完全內建在 App 裡的 Hybrid 模組,由 Native 與 H5 深度協作完成,總共有 4個頁面,其中首頁和製作頁由 H5 製作,而相機頁和儲存頁是複用Native頁面。
專案上線一年累積使用次數已經超過10億次。這套方案經受住了考驗,並在過程中仍然在不斷的優化和擴充。
使用這套實現方案是基於以下幾點考慮:
- 整個模組的風格多變,整體UI是與妝容所搭配的,而整個模組一直都在持續不斷的迭代之中;
- 專案邏輯流程的可變性大,需要H5強大的熱更新能力,及時應對資料的變化,快速的試錯和糾正;
- 拍攝頁與儲存頁是客戶端已經有的模組,可以略微定製後直接複用;
- 需要由客戶端協助接入多套SDK,例如使用演算法SDK進行復雜的影象處理。
簡單看完專案,我們接下來開始 bridge.js 的構建。由於本系列文章主要面向前端童鞋,因此我們主要展開 H5 的部分,即會注入到每個頁面頭部的 bridge.js 的實現,客戶端中的 SDK 部分就不詳細解構了,只會提到一些細節。
搭建地基 — bridge.js 架構
基於上篇文章闡述的結構,我們進一步去完善細節部分,先整理成下面這樣的流程結構圖,大家先看下圖,有個大致的概念:
nativeCall
與 postMessage
這兩個主體 API 橋接了 Native端 和 H5端
接下來我們會細看裡面各個部分的程式碼實現。
(一) 業務方使用姿勢
首先,我們先看下在這套方案中,業務方是如何使用的,下面以獲取網路狀態為例:
(二) H5 –> Native
接下來直接來看 nativeCall
的內部實現:
裡面可以解構成下面4個步驟:
- 生成唯一 handler 標識,從 0 開始累加;
- 將引數按 handler 值的規則存入引數池(_paramsStore)中;
- 以 handler 註冊自定義事件,繫結 callback,並將 callback也存入 _callbackStore 中,
addEvent()
,儲存的目的主要是為了事件解綁時使用; - 以 iframe 的形式傳送協議,並攜帶唯一標識 handler,
send()
;
Native:
- 客戶端接收到請求後,會使用 handler 呼叫
getParam
從引數池中獲取對應的引數。
- 執行協議對應的功能;
這樣即走通了 H5 –> Native 的這個流程,在客戶端完成了對應的功能後,既開始回傳執行結果。
(二) Native –> H5
Native:
- Native 完成功能後,直接呼叫
Bridge.postMessage(handler, data)
,將 執行結果 和 之前nativeCall
傳過來的 標識 回傳給 H5;
H5:
- H5 在接收到唯一標識後初始化對應的自定義事件,掛載資料後觸發,這裡涉及的就是
fireEvent
這個函式:
這樣,我們就已經完成了雙端之間的雙向互動機制了,梳理出了整個 bridge.js 的核心程式碼了,包含了:
- 最重要的開放API:
nativeCall
與postMessage
; - 客戶端獲取引數函式:
getParam
; - 事件回撥系統中的
addEvent
和fireEvent
; - 用於傳送協議的
send
。
安卓相容性:
如果看過上一篇原理篇的童鞋,這時可能會有個疑問:在 Android 4.4以下時,使用的 loadUrl
進行 js 函式的呼叫,而此時是無法獲取函式的返回值的,也就是說4.4- 時,安卓並無法通過 getParam
這個函式來獲取到協議的引數,這裡需要做相容性的處理,而我們這裡可以使用一個曲線救國的騷操作,使用到的原理就是上一篇文章中有提到的另一種 H5 -> Native 的方案:
WebView 中的 prompt 攔截
方案如下:
- 當安卓接受到協議,並拿到 handler 值;
- 使用無相容性問題的
loadUrl
執行 js:Bridge.getParam(handler)
,直接將返回值直接通過 js 中的prompt
發出:
- 通過重寫
onJsPrompt
這個方法,攔截上一步發出的 prompt 的內容,並解析出相應的引數;
通過這樣的方式,安卓全平臺都可以完成引數的獲取,並且方式統一,不需要分平臺相容,這就非常的skrskr啦。~~
現在看下來,是不是覺得炒雞簡單?。分分鐘能寫100個。。沒錯!其實核心的原理就是這麼的簡單,但這只是一個最基礎的地基而已,而基於地基之上,我們就可以開始一層一層建造我們的大樓了!
建造大樓 — 協議的定製
在完成最基礎的架構後,我們就可以開始來進一步完成一些上層建築了,制定一系列真正開放給業務方使用的協議 API,完善整套方案。
首先我們可以將這些協議分成 功能協議 和 業務協議。
功能協議
這類協議是指用於完善整套方案的基礎功能的一些通用協議,以command://
作為通用頭,封裝在 SDK 之中,可以在全線 App、全線 WebView 中使用:
1.初始化機制
上篇文章有提到由於 bridge.js 注入的非同步性,我們需要由客戶端在注入完成後通知 H5。
這裡我們可以約定一個通用的初始化事件,這裡我們約定為 _init_
,因此前端就可以進行入口的監聽, 類似於我們常用的 DOMContentLoaded
:
大家可以看到,這裡用了個標記位用於避免事件被重複觸發,這是由於客戶端中是通過監聽 WebView 的生命週期鉤子來觸發的,而 iframe 之類的操作會導致這些鉤子的多次觸發,因此需要雙方各做一層防禦性措施。
接下來,我們可以通過該事件,直接初始化傳給H5一些環境引數和系統資訊等,下面是我們使用到的:
同樣的,我們可以約定更多的頁面生命週期事件,例如因為App很經常性的隱藏到後臺,因此在被啟用時,我們可以設定個生命週期: _resume_
,可以用於告知 H5 頁面被啟用。
Tips:
這裡就能體現出我們通過事件機制來作為回撥系統的優勢了,我們可以以最習慣的方式進行事件的監聽,而客戶端可以直接使用 bridge.fireEvent('_init_', data)
觸發事件,這樣便可以優雅地實現 Native -> H5 的單方向互動。
2.包更新機制
Hybrid模組 的其中一種方式是將前端程式碼打包後內建於 App 本地,以便擁有最快的啟動效能和離線訪問能力。而這種方式最大的麻煩點,就是程式碼的更新,我們不可能每次有修改時就手動重新打包給客戶端童鞋替換,而且這樣也失去了我們的熱更新機制。
因此這裡就需要一套新的熱更新機制,這套機制需要由客戶端/前端/服務端 三端的童鞋提供對應的資源,共同協作完成整套流程。
資源:
- H5: 每個程式碼包都有一個唯一且遞增的版本號;
- Native: 提供包下載且解壓到對應目錄的服務,前端可以由下面這個協議來呼叫該功能。
- 服務端: 提供一個介面,可以獲取線上最新程式碼包的版本號和下載地址。
流程:
- 前端更新程式碼打包後按版本號上傳至指定的伺服器上;
- 每次開啟頁面時,H5請求介面獲取線上最新程式碼包版本號,並與本地包進行版本號比對,當線上的版本號 大於 本地包版本號時,發起包下載協議:
- 客戶端接受到協議後,直接去線上地址下載最新的程式碼包,並解壓替換到當前目錄檔案。
擁有這樣的機制後,H5在開發後,就可以直接打包將包上傳到對應的伺服器上,這樣在 App 中開啟頁面後,即可以實時的熱更新。
3.環境系統 和 多語言系統
通常,我們會將專案分成多個不同的環境,相互隔離。而由於 Hybrid 模組是置於 App 中的,因此環境需要與 App 進行匹配,這裡就可以直接使用上面第一點提到的,通過 _init_
中攜帶的資料data.env
來匹配:
env: 0 – 正式環境; 1 – 測試環境; 2 – 開發環境;
同理, 多語言也可以直接使用 e.data.language
直接進行匹配;
Tips:
環境機制我們通常主要用於匹配後端的環境,正式環境和測試環境對應不同的介面。而這裡還有一點特別的,就是需要注意程式碼包的更新,上述的包更新條件要包含三個方面: 版本號、環境和 App版本,在不同環境不同 App 版本下,也應該更新到相應的最新程式碼包。
4. 事件中轉站
由於頁面是 H5 開發,而 Native 可能需要控制 H5 頁面,例如最常用的場景:
當頁面中有彈窗或者SPA切換頁面時,安卓的返回實體鍵應該能完成對應的回退,而不是因為 WebView 沒有 history 就直接關閉。
類似於這類需求,這裡就可以定製一個事件中心(_eventListeners_
),用於監聽客戶端的實體返回鍵:
5. 資料傳遞機制
在業務中,很多場景需要做到 Native 與 H5 保持資料的同步,此時就可以使用類似上面的原理,制定一套資料傳遞協議:
Tips:
Hybrid模組通常需要從對應的入口進入,因此這裡有一種可以優化的方式:
由 App 在啟動時先去獲取線上資料,在進入 WebView 後直接通過 _init_
或者觸發 getData
直接傳送給 H5,這樣能減少請求數量,優化使用者體驗。
6. 代理請求
H5中最常用的就是請求,通常我們可以直接使用ajax,但是這裡有幾個問題比較棘手:
- 最常見的請求跨域;
- 資料演算法加密;
- 使用者登入校驗;
而客戶端的請求便不會出現這些問題,因此我們可以由客戶端代理我們發出的請求,可以定製4個協議: getProxy
,postProxy
, getProxyLogined
,postProxyLogined
,其中帶有 Logined
的協議代表著在請求時會自動攜帶已登入使用者的 token 和 uid 等引數,使用在一些需要登入資訊的介面上。這樣做的好處是
- H5 方就無需處理繁多的各項複雜資訊,不需要進行跨端傳輸;
- 能夠對 H5 與 Native 的請求出口進行統一,方便加工處理。
7.更多
除了這些重要的功能外,我們還可以非常自由地定製很多協議,讓 H5 擁有更多更強大的功能,下面是我們所定製的一些功能:
getNetwork
:獲取網路狀態;openApp
:喚起其它 App;setShareInfo
與callShare
:分享內容到第三方平臺;link
:使用新的 WebView 開啟頁面;closeWebview
:關閉 WebView;setStorage
與getStorage
:設定與獲取快取資料;loading
:呼叫客戶端通用 Loading;setWebviewTitle
:設定 WebView 標題;saveImage
:儲存圖片到本地;- …
這裡可以定義更多的通用性協議,這裡有個原則可以遵守,即這部分協議應該是基礎性功能,應該是純淨的,適用於所有的業務方。根據上篇文章提到的理念,這部分是當成通用 SDK 進行維護與升級的,因此不應該耦合業務層的任何邏輯。
而有時我們會遇到需要定製一些業務上的邏輯,例如上面提到的專案中,我們要將使用者圖片通過演算法處理成卡通畫。這樣的需求就是非常的業務化,不適用於其它專案,因此我們應該定製成業務協議。
業務協議
這類協議區別於功能協議,它們會雜合一定程度的業務邏輯,而這些邏輯只是針對於特定的專案。其實對於 H5 的使用上,差別並不大,只是使用對應特殊的協議頭用於區分,例如:
這類協議通常不包含在 SDK 中,因此需要由客戶端的童鞋針對專案的 WebView 進行定製,使用 bridge.js 提供的基礎功能實現對應的複雜功能。而在其它的專案入口中,就無法使用這些協議。
總結
看到總結兩個字,有沒有長舒了一口氣。。。通過這兩篇文章,我們終於將 Hybrid 方案的前端部分完全的解構清楚了,是不是有種神清氣爽的感覺,完全可以馬上開啟你們的 Hybrid 之旅了。鼓掌鼓掌!!!!
但這也遠非終點,或者說這永無終點。~大樓建成後,離真正的摩天大樓還是差著一步 — 內部裝修,其實接下來我們還需要做很多的優化措施,來解決一些仍然存在的問題,這部分其實我們也一直還在努力的階段。
受篇幅所限,有時間會將這部分再寫一篇優化篇,主要來與大家探討下我們所能想到的一些優化方案,非常期待大佬們也能給我們提供更多的建議和解決辦法。感恩~~
更多文章 摸我 閱讀。。