摘要:本文將從以下幾個方面闡述架構設計的一些經驗和思考。原文及討論請到通訊作為一種跨語言開發模式,通訊層是架構首先應該考慮和設計的,往后所有的邏輯都是基于通訊層展開。
關于Hybrid模式開發app的好處,網絡上已有很多文章闡述了,這里不展開。
本文將從以下幾個方面闡述Hybrid app架構設計的一些經驗和思考。
原文及討論請到 github issue
通訊作為一種跨語言開發模式,通訊層是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部分的實現,項目地址:
hybrid-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卡這么簡單粗暴,應該解決以下幾個問題:
H5應該有線上版本
作為訪問離線資源的降級方案,當本地資源不存在的時候應該走現網去拉取對應資源,保證H5可用。另外就是,對于H5,我們不會把所有頁面都使用離線訪問,例如活動頁面,這類快速上線又快速下線的頁面,設計離線訪問方式開發周期比較高,也有可能是頁面完全是動態的,不同的用戶在不同的時間看到的頁面不一樣,沒法落地成靜態頁面,還有一類就是一些說明類的靜態頁面,更新頻率很小的,也沒必要做成離線占用手機存儲空間。
開發調試&抓包
我們知道,基于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容器的設計。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/78409.html
閱讀 2879·2019-08-30 15:44
閱讀 1906·2019-08-29 13:59
閱讀 2851·2019-08-29 12:29
閱讀 1097·2019-08-26 13:57
閱讀 3210·2019-08-26 13:45
閱讀 3340·2019-08-26 10:28
閱讀 849·2019-08-26 10:18
閱讀 1702·2019-08-23 16:52