Hybrid App技術解析 — 實戰篇

發表於2018-08-13

引言

上一篇原理篇,我們已經詳細地闡述了 Hybrid App 的基礎原理,瞭解了 Native端 和 H5端 是如何通訊的,還有 bridge 的設計和接入。而本篇文章將開始把這些原因進一步實踐,用程式碼真正地去實現一套完整且穩定的 Hybrid 方案。如果對原理還有疑問的小夥伴,請移步Hybrid App技術解析 — 原理篇,只有在理解了理論的基礎上,進一步與實踐相結合,才能真正地去深入一項技術。

如果大家有什麼更好的方案或建議,可以到 github.com/xd-tayde 上與我進行討論哈!

摩天大樓

說了那麼一大堆理論知識,可能有小夥伴會說:“ 你是不是吹流弊啊。”。。那就先來簡單介紹下我們已經使用這套方案落地的專案之一。

Hybrid App技術解析 — 實戰篇

這是一個完全內建在 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端

Hybrid App技術解析 — 實戰篇

接下來我們會細看裡面各個部分的程式碼實現。

(一) 業務方使用姿勢

首先,我們先看下在這套方案中,業務方是如何使用的,下面以獲取網路狀態為例:

Hybrid App技術解析 — 實戰篇

(二) H5 –> Native

接下來直接來看 nativeCall 的內部實現:

Hybrid App技術解析 — 實戰篇

裡面可以解構成下面4個步驟:

  1. 生成唯一 handler 標識,從 0 開始累加;
  2. 將引數按 handler 值的規則存入引數池(_paramsStore)中;
  3. 以 handler 註冊自定義事件,繫結 callback,並將 callback也存入 _callbackStore 中,addEvent(),儲存的目的主要是為了事件解綁時使用;
  4. 以 iframe 的形式傳送協議,並攜帶唯一標識 handler,send()

Hybrid App技術解析 — 實戰篇

Native:

  • 客戶端接收到請求後,會使用 handler 呼叫 getParam 從引數池中獲取對應的引數Hybrid App技術解析 — 實戰篇
  • 執行協議對應的功能

這樣即走通了 H5 –> Native 的這個流程,在客戶端完成了對應的功能後,既開始回傳執行結果。

(二) Native –> H5

Native:

  • Native 完成功能後,直接呼叫 Bridge.postMessage(handler, data),將 執行結果 和 之前 nativeCall 傳過來的 標識 回傳給 H5;Hybrid App技術解析 — 實戰篇

H5:

  • H5 在接收到唯一標識後初始化對應的自定義事件,掛載資料後觸發,這裡涉及的就是 fireEvent 這個函式: Hybrid App技術解析 — 實戰篇

這樣,我們就已經完成了雙端之間的雙向互動機制了,梳理出了整個 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 發出:Hybrid App技術解析 — 實戰篇
  • 通過重寫 onJsPrompt 這個方法,攔截上一步發出的 prompt 的內容,並解析出相應的引數;Hybrid App技術解析 — 實戰篇

通過這樣的方式,安卓全平臺都可以完成引數的獲取,並且方式統一,不需要分平臺相容,這就非常的skrskr啦。~~

現在看下來,是不是覺得炒雞簡單?。分分鐘能寫100個。。沒錯!其實核心的原理就是這麼的簡單,但這只是一個最基礎的地基而已,而基於地基之上,我們就可以開始一層一層建造我們的大樓了!

建造大樓 — 協議的定製

在完成最基礎的架構後,我們就可以開始來進一步完成一些上層建築了,制定一系列真正開放給業務方使用的協議 API,完善整套方案。

首先我們可以將這些協議分成 功能協議 和 業務協議

功能協議

這類協議是指用於完善整套方案的基礎功能的一些通用協議,以command://作為通用頭,封裝在 SDK 之中,可以在全線 App、全線 WebView 中使用:

1.初始化機制

上篇文章有提到由於 bridge.js 注入的非同步性,我們需要由客戶端在注入完成後通知 H5。

這裡我們可以約定一個通用的初始化事件,這裡我們約定為 _init_,因此前端就可以進行入口的監聽, 類似於我們常用的 DOMContentLoadedHybrid App技術解析 — 實戰篇

大家可以看到,這裡用了個標記位用於避免事件被重複觸發,這是由於客戶端中是通過監聽 WebView 的生命週期鉤子來觸發的,而 iframe 之類的操作會導致這些鉤子的多次觸發,因此需要雙方各做一層防禦性措施。

接下來,我們可以通過該事件,直接初始化傳給H5一些環境引數和系統資訊等,下面是我們使用到的:Hybrid App技術解析 — 實戰篇

同樣的,我們可以約定更多的頁面生命週期事件,例如因為App很經常性的隱藏到後臺,因此在被啟用時,我們可以設定個生命週期: _resume_,可以用於告知 H5 頁面被啟用。

Tips:
這裡就能體現出我們通過事件機制來作為回撥系統的優勢了,我們可以以最習慣的方式進行事件的監聽,而客戶端可以直接使用 bridge.fireEvent('_init_', data)觸發事件,這樣便可以優雅地實現 Native -> H5 的單方向互動

2.包更新機制

Hybrid模組 的其中一種方式是將前端程式碼打包後內建於 App 本地,以便擁有最快的啟動效能和離線訪問能力。而這種方式最大的麻煩點,就是程式碼的更新,我們不可能每次有修改時就手動重新打包給客戶端童鞋替換,而且這樣也失去了我們的熱更新機制。

因此這裡就需要一套新的熱更新機制,這套機制需要由客戶端/前端/服務端 三端的童鞋提供對應的資源,共同協作完成整套流程。

資源:

  • H5: 每個程式碼包都有一個唯一且遞增的版本號
  • Native: 提供包下載且解壓到對應目錄的服務,前端可以由下面這個協議來呼叫該功能。Hybrid App技術解析 — 實戰篇
  • 服務端: 提供一個介面,可以獲取線上最新程式碼包的版本號和下載地址

流程:

  • 前端更新程式碼打包後按版本號上傳至指定的伺服器上
  • 每次開啟頁面時,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_ ),用於監聽客戶端的實體返回鍵:

Hybrid App技術解析 — 實戰篇

5. 資料傳遞機制

在業務中,很多場景需要做到 Native 與 H5 保持資料的同步,此時就可以使用類似上面的原理,制定一套資料傳遞協議:

Hybrid App技術解析 — 實戰篇

Tips:

Hybrid模組通常需要從對應的入口進入,因此這裡有一種可以優化的方式:

由 App 在啟動時先去獲取線上資料,在進入 WebView 後直接通過 _init_ 或者觸發 getData 直接傳送給 H5,這樣能減少請求數量,優化使用者體驗。

6. 代理請求

H5中最常用的就是請求,通常我們可以直接使用ajax,但是這裡有幾個問題比較棘手:

  • 最常見的請求跨域
  • 資料演算法加密
  • 使用者登入校驗

而客戶端的請求便不會出現這些問題,因此我們可以由客戶端代理我們發出的請求,可以定製4個協議: getProxypostProxy, getProxyLoginedpostProxyLogined,其中帶有 Logined 的協議代表著在請求時會自動攜帶已登入使用者的 token 和 uid 等引數,使用在一些需要登入資訊的介面上。這樣做的好處是

  • H5 方就無需處理繁多的各項複雜資訊,不需要進行跨端傳輸;
  • 能夠對 H5 與 Native 的請求出口進行統一,方便加工處理。

Hybrid App技術解析 — 實戰篇

7.更多

除了這些重要的功能外,我們還可以非常自由地定製很多協議,讓 H5 擁有更多更強大的功能,下面是我們所定製的一些功能:

  • getNetwork:獲取網路狀態;
  • openApp:喚起其它 App;
  • setShareInfocallShare:分享內容到第三方平臺;
  • link:使用新的 WebView 開啟頁面;
  • closeWebview:關閉 WebView;
  • setStorage 與 getStorage:設定與獲取快取資料;
  • loading:呼叫客戶端通用 Loading;
  • setWebviewTitle:設定 WebView 標題;
  • saveImage:儲存圖片到本地;

這裡可以定義更多的通用性協議,這裡有個原則可以遵守,即這部分協議應該是基礎性功能,應該是純淨的,適用於所有的業務方。根據上篇文章提到的理念,這部分是當成通用 SDK 進行維護與升級的,因此不應該耦合業務層的任何邏輯。

而有時我們會遇到需要定製一些業務上的邏輯,例如上面提到的專案中,我們要將使用者圖片通過演算法處理成卡通畫。這樣的需求就是非常的業務化,不適用於其它專案,因此我們應該定製成業務協議。

業務協議

這類協議區別於功能協議,它們會雜合一定程度的業務邏輯,而這些邏輯只是針對於特定的專案。其實對於 H5 的使用上,差別並不大,只是使用對應特殊的協議頭用於區分,例如:

Hybrid App技術解析 — 實戰篇

這類協議通常不包含在 SDK 中,因此需要由客戶端的童鞋針對專案的 WebView 進行定製,使用 bridge.js 提供的基礎功能實現對應的複雜功能。而在其它的專案入口中,就無法使用這些協議。

總結

看到總結兩個字,有沒有長舒了一口氣。。。通過這兩篇文章,我們終於將 Hybrid 方案的前端部分完全的解構清楚了,是不是有種神清氣爽的感覺,完全可以馬上開啟你們的 Hybrid 之旅了。鼓掌鼓掌!!!!

但這也遠非終點,或者說這永無終點。~大樓建成後,離真正的摩天大樓還是差著一步 — 內部裝修,其實接下來我們還需要做很多的優化措施,來解決一些仍然存在的問題,這部分其實我們也一直還在努力的階段。

受篇幅所限,有時間會將這部分再寫一篇優化篇,主要來與大家探討下我們所能想到的一些優化方案,非常期待大佬們也能給我們提供更多的建議和解決辦法。感恩~~

更多文章 摸我 閱讀。。

相關文章