1. 程式人生 > >Hybrid APP架構設計思路

Hybrid APP架構設計思路

轉自: https://segmentfault.com/a/1190000004263182

關於Hybrid模式開發app的好處,網路上已有很多文章闡述了,這裡不展開。

本文將從以下幾個方面闡述Hybrid app架構設計的一些經驗和思考。

通訊

作為一種跨語言開發模式,通訊層是Hybrid架構首先應該考慮和設計的,往後所有的邏輯都是基於通訊層展開。

Native(以Android為例)和H5通訊,基本原理:

  • Android呼叫H5:通過webview類的loadUrl方法可以直接執行js程式碼,類似瀏覽器位址列輸入一段js一樣的效果

    webview.loadUrl("javascript: alert('hello world')"
    );
  • H5呼叫Android:webview可以攔截H5發起的任意url請求,webview通過約定的規則對攔截到的url進行處理(消費),即可實現H5呼叫Android

    var ifm = document.createElement('iframe');
    ifm.src = 'jsbridge://namespace.method?[...args]';

JSBridge即我們通常說的橋協議,基本的通訊原理很簡單,接下來就是橋協議具體實現。

P.S:註冊私有協議的做法很常見,我們經常遇到的在網頁里拉起一個系統app就是採用私有協議實現的。app在安裝完成之後會註冊私有協議到OS,瀏覽器發現自身不能識別的協議(http、https、file等)時,會將連結拋給OS,OS會尋找可識別此協議的app並用該app處理連結。比如在網頁裡以itunes://

開頭的連結是Apple Store的私有協議,點選後可以啟動Apple Store並且跳轉到相應的介面。國內軟體開發商也經常這麼做,比如支付寶的私有協議alipay://,騰訊的tencent://等等。

橋協議的具體實現

由於JavaScript語言自身的特殊性(單程序),為了不阻塞主程序並且保證H5呼叫的有序性,與Native通訊時對於需要獲取結果的介面(GET類),採用類似於JSONP的設計理念:

類比HTTP的request和response物件,呼叫方會將呼叫的api、引數、以及請求籤名(由呼叫方生成)帶上傳給被呼叫方,被呼叫方處理完之後會吧結果以及請求籤名回傳呼叫方,呼叫方再根據請求籤名找到本次請求對應的回撥函式並執行,至此完成了一次通訊閉環。

H5呼叫Native(以Android為例)示意圖:

Native(以Android為例)呼叫H5示意圖:

基於橋協議的api設計(HybridApi)

jsbridge作為一種通用私有協議,一般會在團隊級或者公司級產品進行共享,所以需要和業務層進行解耦,將jsbridge的內部細節進行封裝,對外暴露平臺級的API。

以下是筆者剝離公司業務程式碼後抽象出的一份HybridApi js部分的實現,專案地址:

另外,對於Native提供的各種介面,也可以簡單封裝下,使之更貼近前端工程師的使用習慣:

// /lib/jsbridge/core.js
function assignAPI(name, callback) {
    var names = name.split(/\./);
    var ns = names.shift();

    var fnName = names.pop();
    var root = createNamespace(JSBridge[ns], names);

    if(fnName) root[fnName] = callback || function() {};
}

增加api:

// /lib/jsbridge/api.js
var assign = require('./core.js').assignAPI;
...
assign('util.compassImage', function(path, callback, quality, width, height) {
    JSBridge.invokeApp('os.getInfo', {
        path: path,
        quality: quality || 80,
        width: width || 'auto',
        height: height || 'auto',
        callback: callback
    });
});

H5上層應用呼叫:

// h5/music/index.js
JSBridge.util.compassImage('http://cdn.foo.com/images/bar.png', function(r) {
    console.log(r.value); // => base64 data
});

介面與互動(Native與H5職責劃分)

本質上,Native和H5都能完成介面開發。幾乎所有hybrid的開發模式都會碰到同樣的一個問題:哪些由Native負責哪些由H5負責?

這個回到原始的問題上來:我們為什麼要採用hybrid模式開發?簡而言之就是同時利用H5的跨平臺、快速迭代能力以及Native的流暢性、系統API呼叫能力。

根據這個原則,為了充分利用二者的優勢,應該儘可能地將app內容使用H5來呈現,而對於js語言本身的缺陷,應該使用Native語言來彌補,如轉場動畫、多執行緒作業(密集型任務)、IO效能等。即總的原則是H5提供內容,Native提供容器,在有可能的條件下對Android原生webview進行優化和改造(參考阿里Hybrid容器的JSM),提升H5的渲染效率。

但是,在實際的專案中,將整個app所有介面都使用H5來開發也有不妥之處,根據經驗,以下情形還是使用Native介面為好:

關鍵介面、互動性強的的介面使用Native

因H5比較容易被惡意攻擊,對於安全性要求比較高的介面,如註冊介面、登陸、支付等介面,會採用Native來取代H5開發,保證資料的安全性,這些頁面通常UI變更的頻率也不高。

對於這些介面,降級的方案也有,就是HTTPS。但是想說的是在國內的若網路環境下,HTTPS的體驗實在是不咋地(主要是慢),而且只能走現網不能走離線通道。

另外,H5本身的動畫開發成本比較高,在低端機器上可能有些繞不過的效能坎,原生js對於手勢的支援也比較弱,因此對於這些型別的介面,可以選擇使用Native來實現,這也是Native本身的優勢不是。比如要實現下面這個音樂播放介面,用H5開發門檻不小吧,留意下中間的波浪線背景,手指左右滑動可以切換動畫。

導航元件採用Native

導航元件,就是頁面的頭元件,左上角一般都是一個back鍵,中間一般都是介面的標題,右邊的話有時是一個隱藏的懸浮選單觸發按鈕有時則什麼也沒有。

移動端有一個特性就是介面下拉有個回彈效果,頭不動body部分跟著滑動,這種效果H5比較難實現。

再者,也是最重要的一點,如果整個介面都是H5的,在H5載入過程中介面將是白屏,在弱網路下使用者可能會很疑惑。

所以基於這兩點,開啟的介面都是Native的導航元件+webview來組成,這樣即使H5載入失敗或者太慢使用者可以選擇直接關閉。

在API層面,會相應的有一個介面來實現這一邏輯(例如叫JSBridge.layout.setHeader),下面程式碼演示定製一個只有back鍵和標題的導航元件:

// /h5/pages/index.js
JSBridge.layout.setHeader({
    background: {
        color: '#00FF00',
        opacity: 0.8
    },
    buttons: [
        // 預設只有back鍵,並且back鍵的預設點選處理函式就是back()
        {
            icon: '../images/back.png',
            width: 16,
            height: 16,
            onClick: function() {
                // todo...
                JSBridge.back();
            }
        },
        {
            text: '音樂首頁',
            color: '#00FF00',
            fontSize: 14,
            left: 10
        }
    ]
});

上面的介面,可以滿足絕大多數的需求,但是還有一些特殊的介面,通過H5程式碼控制生成導航元件這種方式達不到需求:

如上圖所示,介面含有tab,且可以左右滑動切換,tab標題的下劃線會跟著手勢左右滑動。大多見於app的首頁(mainActivity)或者分頻道首頁,這種介面一般採用定製webview的做法:定製的導航元件和內容框架(為了支援左右滑動手勢),H5開啟此類介面一般也是開特殊的API:

// /h5/pages/index.js
// 開打音樂頻道下“我的音樂”tab
JSBridge.view.openMusic({'tab': 'personal'});

這種開啟特殊的介面的API之所以特殊,是因為它內部要麼是純Native實現,要麼是和某個約定的html檔案繫結,呼叫時開啟指定的html。假設這個例子中,tab內容是H5的,如果H5是SPA架構的那麼openMusic({'tab': 'personal'})則對應/music.html#personal這個url,反之多頁面的則可能對應/mucic-personal.html

至於一般的開啟新介面,則有兩種可能:

  • app內H5介面

    指的是由app開發者開發的H5頁面,也即是app的功能介面,一般互相跳轉需要轉場動畫,開啟方式是採用Native提供的介面開啟,例如:
    
    JSBridge.view.openUrl({
        url: '/music-list.html',
        title: '音樂列表'
    });
    再配合下面即將提到的離線訪問方式,基本可以做到模擬Native介面的效果。
    
  • 第三方H5頁面

    指的是app內嵌的第三方頁面,一般由`a`標籤直接開啟,沒有轉場動畫,但是要求開啟webview預設的歷史列表,以免開啟多個連結後點回退直接回到Native主介面。
    

系統級UI元件採用Native

基於以下原因,一些通用的UI元件,如alert、toast等將採用Native來實現:

  • H5本身有這些元件,但是通常比較簡陋,不能和APP UI風格統一,需要再定製,比如alert元件背景增加遮罩層

  • H5來實現這些元件有時會存在座標、尺寸計算誤差,比如筆者之前遇到的是頁面load異常需要呼叫對話方塊元件提示,但是這時候頁面高度為0,所以會出現彈窗“消失”的現象

  • 這些元件通常功能單一但是通用,適合做成公用元件整合到HybridApi裡邊

下面程式碼演示H5呼叫Native提供的UI元件:

JSBridge.ui.toast('Hello world!');

預設介面採用Native

由於H5是在H5容器裡進行載入和渲染,所以Native很容易對H5頁面的行為進行監控,包括進度條、loading動畫、404監控、5xx監控、網路診斷等,並且在H5載入異常時提供預設介面供使用者操作,防止APP“假死”。

下面是微信的5xx介面示意:

設計H5容器

Native除了負責部分介面開發和公共UI元件設計之外,作為H5的runtime,H5容器是hybrid架構的核心部分,為了讓H5執行更快速穩定和健壯,還應當提供並但不侷限於下面幾方面。

H5離線訪問

之所以選擇hybrid方式來開發,其中一個原因就是要解決webapp訪問慢的問題。即使我們的H5效能優化做的再好伺服器在牛逼,碰到蝸牛一樣的運營商網路你也沒轍,有時候還會碰到流氓運營商再給webapp插點廣告。。。哎說多了都是淚。

離線訪問,顧名思義就是將H5預先放到使用者手機,這樣訪問時就不會再走網路從而做到看起來和Native APP一樣的快了。

但是離線機制絕不是把H5打包解壓到手機sd卡這麼簡單粗暴,應該解決以下幾個問題:

  1. H5應該有線上版本

    作為訪問離線資源的降級方案,當本地資源不存在的時候應該走現網去拉取對應資源,保證H5可用。另外就是,對於H5,我們不會把所有頁面都使用離線訪問,例如活動頁面,這類快速上線又快速下線的頁面,設計離線訪問方式開發週期比較高,也有可能是頁面完全是動態的,不同的使用者在不同的時間看到的頁面不一樣,沒法落地成靜態頁面,還有一類就是一些說明類的靜態頁面,更新頻率很小的,也沒必要做成離線佔用手機儲存空間。
    
  2. 開發除錯&抓包

    我們知道,基於file協議開發是完全基於開發機的,程式碼必須存放於物理機器,這意味著修改程式碼需要push到sd卡再看效果,雖然可以通過假連結訪問開發機本地server釋出時移除的方式,但是個人覺得還是太麻煩易出錯。
    

為了實現同一資源的線上和離線訪問,Native需要對H5的靜態資源請求進行攔截判斷,將靜態資源“對映”到sd卡資源,即實現一個處理H5資源的本地路由,實現這一邏輯的模組暫且稱之為Local Url Router,具體實現細節在文章後面。

H5離線動態更新機制

將H5資源放置到本地離線訪問,最大的挑戰就是本地資源的動態更新如何設計,這部分可以說是最複雜的了,因為這同時涉及到H5、Native和伺服器三方,覆蓋式離線更新示意圖如下:

解釋下上圖,開發階段H5程式碼可以通過手機設定HTTP代理方式直接訪問開發機。完成開發之後,將H5程式碼推送到管理平臺進行構建、打包,然後管理平臺再通過事先設計好的長連線通道將H5新版本資訊推送給客戶端,客戶端收到更新指令後開始下載新包、對包進行完整性校驗、merge回本地對應的包,更新結束。

其中,管理平臺推送給客戶端的資訊主要包括專案名(包名)、版本號、更新策略(增量or全量)、包CDN地址、MD5等。

通常來說,H5資源分為兩種,經常更新的業務程式碼和不經常更新的框架、庫程式碼和公用元件程式碼,為了實現離線資源的共享,在H5打包時可以採用分包的策略,將公用部分單獨打包,在本地也是單獨存放,分包及合併示意圖:

Local Url Router

離線資源更新的問題解決了,剩下的就是如何使用離線資源了。

上面已經提到,對於H5的請求,線上和離線採用相同的url訪問,這就需要H5容器對H5的資源請求進行攔截“對映”到本地,即Local Url Router

Local Url Router主要負責H5靜態資源請求的分發(線上資源到sd卡資源的對映),但是不管是白名單還是過濾靜態檔案型別,Native攔截規則和對映規則將變得比較複雜。這裡,阿里去啊app的思路就比較贊,我們借鑑一下,將對映規則交給H5去生成:H5開發完成之後會掃描H5專案然後生成一份線上資源和離線資源路徑的對映表(souce-router.json),H5容器只需負責解析這個對映表即可。

H5資源包解壓之後在本地的目錄結構類似:

$ cd h5 && tree
.
├── js/
├── css/
├── img/
├── pages
│   ├── index.html
│   └── list.html
└── souce-router.json

souce-router.json的資料結構類似:

{
    "protocol": "http",
    "host": "o2o.xx.com",
    "localRoot": "[/storage/0/data/h5/o2o/]",
    "localFolder": "o2o.xx.com",
    "rules": {
        "/index.html": "pages/index.html",
        "/js/": "js/"
    }
}

H5容器攔截到靜態資源請求時,如果本地有對應的檔案則直接讀取本地檔案返回,否則發起HTTP請求獲取線上資源,如果設計完整一點還可以考慮同時開啟新執行緒去下載這個資源到本地,下次就走離線了。

下圖演示資源在app內部的訪問流程圖:

其中proxy指的是開發時手機設定代理http代理到開發機。

資料通道

  • 上報

由於介面由H5和Native共同完成,介面上的使用者互動埋點資料最好由H5容器統一採集、上報,還有,由頁面跳轉產生的瀏覽軌跡(轉化漏斗),也由H5容器記錄和上報

  • ajax代理

因ajax受同源策略限制,可以在hybridApi層對ajax進行統一封裝,同時相容H5容器和瀏覽器runtime,採用更高效的通訊通道加速H5的資料傳輸

Native對H5的擴充套件

主要指擴充套件H5的硬體介面呼叫能力,比如螢幕旋轉、攝像頭、麥克風、位置服務等等,將Native的能力通過介面的形式提供給H5。

綜述

最後來張圖總結下,hybrid客戶端整體架構圖:

其中的Synchronize Service模組表示和伺服器的長連線通訊模組,用於接受伺服器端各種推送,包括離線包等。Source Merge Service模組表示對解壓後的H5資源進行更新,包括增加檔案、以舊換新以及刪除過期檔案等。

可以看到,hybrid模式的app架構,最核心和最難的部分都是H5容器的設計。