摘要:本文將從以下幾個(gè)方面闡述架構(gòu)設(shè)計(jì)的一些經(jīng)驗(yàn)和思考。原文及討論請(qǐng)到通訊作為一種跨語(yǔ)言開(kāi)發(fā)模式,通訊層是架構(gòu)首先應(yīng)該考慮和設(shè)計(jì)的,往后所有的邏輯都是基于通訊層展開(kāi)。
關(guān)于Hybrid模式開(kāi)發(fā)app的好處,網(wǎng)絡(luò)上已有很多文章闡述了,這里不展開(kāi)。
本文將從以下幾個(gè)方面闡述Hybrid app架構(gòu)設(shè)計(jì)的一些經(jīng)驗(yàn)和思考。
原文及討論請(qǐng)到 github issue
通訊作為一種跨語(yǔ)言開(kāi)發(fā)模式,通訊層是Hybrid架構(gòu)首先應(yīng)該考慮和設(shè)計(jì)的,往后所有的邏輯都是基于通訊層展開(kāi)。
Native(以Android為例)和H5通訊,基本原理:
Android調(diào)用H5:通過(guò)webview類(lèi)的loadUrl方法可以直接執(zhí)行js代碼,類(lèi)似瀏覽器地址欄輸入一段js一樣的效果
webview.loadUrl("javascript: alert("hello world")");
H5調(diào)用Android:webview可以攔截H5發(fā)起的任意url請(qǐng)求,webview通過(guò)約定的規(guī)則對(duì)攔截到的url進(jìn)行處理(消費(fèi)),即可實(shí)現(xiàn)H5調(diào)用Android
var ifm = document.createElement("iframe"); ifm.src = "jsbridge://namespace.method?[...args]";
JSBridge即我們通常說(shuō)的橋協(xié)議,基本的通訊原理很簡(jiǎn)單,接下來(lái)就是橋協(xié)議具體實(shí)現(xiàn)。
P.S:注冊(cè)私有協(xié)議的做法很常見(jiàn),我們經(jīng)常遇到的在網(wǎng)頁(yè)里拉起一個(gè)系統(tǒng)app就是采用私有協(xié)議實(shí)現(xiàn)的。app在安裝完成之后會(huì)注冊(cè)私有協(xié)議到OS,瀏覽器發(fā)現(xiàn)自身不能識(shí)別的協(xié)議(http、https、file等)時(shí),會(huì)將鏈接拋給OS,OS會(huì)尋找可識(shí)別此協(xié)議的app并用該app處理鏈接。比如在網(wǎng)頁(yè)里以itunes://開(kāi)頭的鏈接是Apple Store的私有協(xié)議,點(diǎn)擊后可以啟動(dòng)Apple Store并且跳轉(zhuǎn)到相應(yīng)的界面。國(guó)內(nèi)軟件開(kāi)發(fā)商也經(jīng)常這么做,比如支付寶的私有協(xié)議alipay://,騰訊的tencent://等等。
橋協(xié)議的具體實(shí)現(xiàn)由于JavaScript語(yǔ)言自身的特殊性(單進(jìn)程),為了不阻塞主進(jìn)程并且保證H5調(diào)用的有序性,與Native通訊時(shí)對(duì)于需要獲取結(jié)果的接口(GET類(lèi)),采用類(lèi)似于JSONP的設(shè)計(jì)理念:
類(lèi)比HTTP的request和response對(duì)象,調(diào)用方會(huì)將調(diào)用的api、參數(shù)、以及請(qǐng)求簽名(由調(diào)用方生成)帶上傳給被調(diào)用方,被調(diào)用方處理完之后會(huì)吧結(jié)果以及請(qǐng)求簽名回傳調(diào)用方,調(diào)用方再根據(jù)請(qǐng)求簽名找到本次請(qǐng)求對(duì)應(yīng)的回調(diào)函數(shù)并執(zhí)行,至此完成了一次通訊閉環(huán)。
H5調(diào)用Native(以Android為例)示意圖:
Native(以Android為例)調(diào)用H5示意圖:
基于橋協(xié)議的api設(shè)計(jì)(HybridApi)jsbridge作為一種通用私有協(xié)議,一般會(huì)在團(tuán)隊(duì)級(jí)或者公司級(jí)產(chǎn)品進(jìn)行共享,所以需要和業(yè)務(wù)層進(jìn)行解耦,將jsbridge的內(nèi)部細(xì)節(jié)進(jìn)行封裝,對(duì)外暴露平臺(tái)級(jí)的API。
以下是筆者剝離公司業(yè)務(wù)代碼后抽象出的一份HybridApi js部分的實(shí)現(xiàn),項(xiàng)目地址:
hybrid-js
另外,對(duì)于Native提供的各種接口,也可以簡(jiǎn)單封裝下,使之更貼近前端工程師的使用習(xí)慣:
// /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上層應(yīng)用調(diào)用:
// h5/music/index.js JSBridge.util.compassImage("http://cdn.foo.com/images/bar.png", function(r) { console.log(r.value); // => base64 data });界面與交互(Native與H5職責(zé)劃分)
本質(zhì)上,Native和H5都能完成界面開(kāi)發(fā)。幾乎所有hybrid的開(kāi)發(fā)模式都會(huì)碰到同樣的一個(gè)問(wèn)題:哪些由Native負(fù)責(zé)哪些由H5負(fù)責(zé)?
這個(gè)回到原始的問(wèn)題上來(lái):我們?yōu)槭裁匆捎胔ybrid模式開(kāi)發(fā)?簡(jiǎn)而言之就是同時(shí)利用H5的跨平臺(tái)、快速迭代能力以及Native的流暢性、系統(tǒng)API調(diào)用能力。
根據(jù)這個(gè)原則,為了充分利用二者的優(yōu)勢(shì),應(yīng)該盡可能地將app內(nèi)容使用H5來(lái)呈現(xiàn),而對(duì)于js語(yǔ)言本身的缺陷,應(yīng)該使用Native語(yǔ)言來(lái)彌補(bǔ),如轉(zhuǎn)場(chǎng)動(dòng)畫(huà)、多線程作業(yè)(密集型任務(wù))、IO性能等。即總的原則是H5提供內(nèi)容,Native提供容器,在有可能的條件下對(duì)Android原生webview進(jìn)行優(yōu)化和改造(參考阿里Hybrid容器的JSM),提升H5的渲染效率。
但是,在實(shí)際的項(xiàng)目中,將整個(gè)app所有界面都使用H5來(lái)開(kāi)發(fā)也有不妥之處,根據(jù)經(jīng)驗(yàn),以下情形還是使用Native界面為好:
關(guān)鍵界面、交互性強(qiáng)的的界面使用Native因H5比較容易被惡意攻擊,對(duì)于安全性要求比較高的界面,如注冊(cè)界面、登陸、支付等界面,會(huì)采用Native來(lái)取代H5開(kāi)發(fā),保證數(shù)據(jù)的安全性,這些頁(yè)面通常UI變更的頻率也不高。
對(duì)于這些界面,降級(jí)的方案也有,就是HTTPS。但是想說(shuō)的是在國(guó)內(nèi)的若網(wǎng)絡(luò)環(huán)境下,HTTPS的體驗(yàn)實(shí)在是不咋地(主要是慢),而且只能走現(xiàn)網(wǎng)不能走離線通道。
另外,H5本身的動(dòng)畫(huà)開(kāi)發(fā)成本比較高,在低端機(jī)器上可能有些繞不過(guò)的性能坎,原生js對(duì)于手勢(shì)的支持也比較弱,因此對(duì)于這些類(lèi)型的界面,可以選擇使用Native來(lái)實(shí)現(xiàn),這也是Native本身的優(yōu)勢(shì)不是。比如要實(shí)現(xiàn)下面這個(gè)音樂(lè)播放界面,用H5開(kāi)發(fā)門(mén)檻不小吧,留意下中間的波浪線背景,手指左右滑動(dòng)可以切換動(dòng)畫(huà)。
導(dǎo)航組件采用Native導(dǎo)航組件,就是頁(yè)面的頭組件,左上角一般都是一個(gè)back鍵,中間一般都是界面的標(biāo)題,右邊的話有時(shí)是一個(gè)隱藏的懸浮菜單觸發(fā)按鈕有時(shí)則什么也沒(méi)有。
移動(dòng)端有一個(gè)特性就是界面下拉有個(gè)回彈效果,頭不動(dòng)body部分跟著滑動(dòng),這種效果H5比較難實(shí)現(xiàn)。
再者,也是最重要的一點(diǎn),如果整個(gè)界面都是H5的,在H5加載過(guò)程中界面將是白屏,在弱網(wǎng)絡(luò)下用戶可能會(huì)很疑惑。
所以基于這兩點(diǎn),打開(kāi)的界面都是Native的導(dǎo)航組件+webview來(lái)組成,這樣即使H5加載失敗或者太慢用戶可以選擇直接關(guān)閉。
在API層面,會(huì)相應(yīng)的有一個(gè)接口來(lái)實(shí)現(xiàn)這一邏輯(例如叫JSBridge.layout.setHeader),下面代碼演示定制一個(gè)只有back鍵和標(biāo)題的導(dǎo)航組件:
// /h5/pages/index.js JSBridge.layout.setHeader({ background: { color: "#00FF00", opacity: 0.8 }, buttons: [ // 默認(rèn)只有back鍵,并且back鍵的默認(rèn)點(diǎn)擊處理函數(shù)就是back() { icon: "../images/back.png", width: 16, height: 16, onClick: function() { // todo... JSBridge.back(); } }, { text: "音樂(lè)首頁(yè)", color: "#00FF00", fontSize: 14, left: 10 } ] });
上面的接口,可以滿足絕大多數(shù)的需求,但是還有一些特殊的界面,通過(guò)H5代碼控制生成導(dǎo)航組件這種方式達(dá)不到需求:
如上圖所示,界面含有tab,且可以左右滑動(dòng)切換,tab標(biāo)題的下劃線會(huì)跟著手勢(shì)左右滑動(dòng)。大多見(jiàn)于app的首頁(yè)(mainActivity)或者分頻道首頁(yè),這種界面一般采用定制webview的做法:定制的導(dǎo)航組件和內(nèi)容框架(為了支持左右滑動(dòng)手勢(shì)),H5打開(kāi)此類(lèi)界面一般也是開(kāi)特殊的API:
// /h5/pages/index.js // 開(kāi)打音樂(lè)頻道下“我的音樂(lè)”tab JSBridge.view.openMusic({"tab": "personal"});
這種打開(kāi)特殊的界面的API之所以特殊,是因?yàn)樗鼉?nèi)部要么是純Native實(shí)現(xiàn),要么是和某個(gè)約定的html文件綁定,調(diào)用時(shí)打開(kāi)指定的html。假設(shè)這個(gè)例子中,tab內(nèi)容是H5的,如果H5是SPA架構(gòu)的那么openMusic({"tab": "personal"})則對(duì)應(yīng)/music.html#personal這個(gè)url,反之多頁(yè)面的則可能對(duì)應(yīng)/mucic-personal.html。
至于一般的打開(kāi)新界面,則有兩種可能:
app內(nèi)H5界面
指的是由app開(kāi)發(fā)者開(kāi)發(fā)的H5頁(yè)面,也即是app的功能界面,一般互相跳轉(zhuǎn)需要轉(zhuǎn)場(chǎng)動(dòng)畫(huà),打開(kāi)方式是采用Native提供的接口打開(kāi),例如:
JSBridge.view.openUrl({ url: "/music-list.html", title: "音樂(lè)列表" });
再配合下面即將提到的離線訪問(wèn)方式,基本可以做到模擬Native界面的效果。
第三方H5頁(yè)面
指的是app內(nèi)嵌的第三方頁(yè)面,一般由`a`標(biāo)簽直接打開(kāi),沒(méi)有轉(zhuǎn)場(chǎng)動(dòng)畫(huà),但是要求打開(kāi)webview默認(rèn)的歷史列表,以免打開(kāi)多個(gè)鏈接后點(diǎn)回退直接回到Native主界面。系統(tǒng)級(jí)UI組件采用Native
基于以下原因,一些通用的UI組件,如alert、toast等將采用Native來(lái)實(shí)現(xiàn):
H5本身有這些組件,但是通常比較簡(jiǎn)陋,不能和APP UI風(fēng)格統(tǒng)一,需要再定制,比如alert組件背景增加遮罩層
H5來(lái)實(shí)現(xiàn)這些組件有時(shí)會(huì)存在坐標(biāo)、尺寸計(jì)算誤差,比如筆者之前遇到的是頁(yè)面load異常需要調(diào)用對(duì)話框組件提示,但是這時(shí)候頁(yè)面高度為0,所以會(huì)出現(xiàn)彈窗“消失”的現(xiàn)象
這些組件通常功能單一但是通用,適合做成公用組件整合到HybridApi里邊
下面代碼演示H5調(diào)用Native提供的UI組件:
JSBridge.ui.toast("Hello world!");默認(rèn)界面采用Native
由于H5是在H5容器里進(jìn)行加載和渲染,所以Native很容易對(duì)H5頁(yè)面的行為進(jìn)行監(jiān)控,包括進(jìn)度條、loading動(dòng)畫(huà)、404監(jiān)控、5xx監(jiān)控、網(wǎng)絡(luò)診斷等,并且在H5加載異常時(shí)提供默認(rèn)界面供用戶操作,防止APP“假死”。
下面是微信的5xx界面示意:
設(shè)計(jì)H5容器Native除了負(fù)責(zé)部分界面開(kāi)發(fā)和公共UI組件設(shè)計(jì)之外,作為H5的runtime,H5容器是hybrid架構(gòu)的核心部分,為了讓H5運(yùn)行更快速穩(wěn)定和健壯,還應(yīng)當(dāng)提供并但不局限于下面幾方面。
H5離線訪問(wèn)之所以選擇hybrid方式來(lái)開(kāi)發(fā),其中一個(gè)原因就是要解決webapp訪問(wèn)慢的問(wèn)題。即使我們的H5性能優(yōu)化做的再好服務(wù)器在牛逼,碰到蝸牛一樣的運(yùn)營(yíng)商網(wǎng)絡(luò)你也沒(méi)轍,有時(shí)候還會(huì)碰到流氓運(yùn)營(yíng)商再給webapp插點(diǎn)廣告。。。哎說(shuō)多了都是淚。
離線訪問(wèn),顧名思義就是將H5預(yù)先放到用戶手機(jī),這樣訪問(wèn)時(shí)就不會(huì)再走網(wǎng)絡(luò)從而做到看起來(lái)和Native APP一樣的快了。
但是離線機(jī)制絕不是把H5打包解壓到手機(jī)sd卡這么簡(jiǎn)單粗暴,應(yīng)該解決以下幾個(gè)問(wèn)題:
H5應(yīng)該有線上版本
作為訪問(wèn)離線資源的降級(jí)方案,當(dāng)本地資源不存在的時(shí)候應(yīng)該走現(xiàn)網(wǎng)去拉取對(duì)應(yīng)資源,保證H5可用。另外就是,對(duì)于H5,我們不會(huì)把所有頁(yè)面都使用離線訪問(wèn),例如活動(dòng)頁(yè)面,這類(lèi)快速上線又快速下線的頁(yè)面,設(shè)計(jì)離線訪問(wèn)方式開(kāi)發(fā)周期比較高,也有可能是頁(yè)面完全是動(dòng)態(tài)的,不同的用戶在不同的時(shí)間看到的頁(yè)面不一樣,沒(méi)法落地成靜態(tài)頁(yè)面,還有一類(lèi)就是一些說(shuō)明類(lèi)的靜態(tài)頁(yè)面,更新頻率很小的,也沒(méi)必要做成離線占用手機(jī)存儲(chǔ)空間。
開(kāi)發(fā)調(diào)試&抓包
我們知道,基于file協(xié)議開(kāi)發(fā)是完全基于開(kāi)發(fā)機(jī)的,代碼必須存放于物理機(jī)器,這意味著修改代碼需要push到sd卡再看效果,雖然可以通過(guò)假鏈接訪問(wèn)開(kāi)發(fā)機(jī)本地server發(fā)布時(shí)移除的方式,但是個(gè)人覺(jué)得還是太麻煩易出錯(cuò)。
為了實(shí)現(xiàn)同一資源的線上和離線訪問(wèn),Native需要對(duì)H5的靜態(tài)資源請(qǐng)求進(jìn)行攔截判斷,將靜態(tài)資源“映射”到sd卡資源,即實(shí)現(xiàn)一個(gè)處理H5資源的本地路由,實(shí)現(xiàn)這一邏輯的模塊暫且稱之為Local Url Router,具體實(shí)現(xiàn)細(xì)節(jié)在文章后面。
H5離線動(dòng)態(tài)更新機(jī)制將H5資源放置到本地離線訪問(wèn),最大的挑戰(zhàn)就是本地資源的動(dòng)態(tài)更新如何設(shè)計(jì),這部分可以說(shuō)是最復(fù)雜的了,因?yàn)檫@同時(shí)涉及到H5、Native和服務(wù)器三方,覆蓋式離線更新示意圖如下:
解釋下上圖,開(kāi)發(fā)階段H5代碼可以通過(guò)手機(jī)設(shè)置HTTP代理方式直接訪問(wèn)開(kāi)發(fā)機(jī)。完成開(kāi)發(fā)之后,將H5代碼推送到管理平臺(tái)進(jìn)行構(gòu)建、打包,然后管理平臺(tái)再通過(guò)事先設(shè)計(jì)好的長(zhǎng)連接通道將H5新版本信息推送給客戶端,客戶端收到更新指令后開(kāi)始下載新包、對(duì)包進(jìn)行完整性校驗(yàn)、merge回本地對(duì)應(yīng)的包,更新結(jié)束。
其中,管理平臺(tái)推送給客戶端的信息主要包括項(xiàng)目名(包名)、版本號(hào)、更新策略(增量or全量)、包CDN地址、MD5等。
通常來(lái)說(shuō),H5資源分為兩種,經(jīng)常更新的業(yè)務(wù)代碼和不經(jīng)常更新的框架、庫(kù)代碼和公用組件代碼,為了實(shí)現(xiàn)離線資源的共享,在H5打包時(shí)可以采用分包的策略,將公用部分多帶帶打包,在本地也是多帶帶存放,分包及合并示意圖:
Local Url Router離線資源更新的問(wèn)題解決了,剩下的就是如何使用離線資源了。
上面已經(jīng)提到,對(duì)于H5的請(qǐng)求,線上和離線采用相同的url訪問(wèn),這就需要H5容器對(duì)H5的資源請(qǐng)求進(jìn)行攔截“映射”到本地,即Local Url Router。
Local Url Router主要負(fù)責(zé)H5靜態(tài)資源請(qǐng)求的分發(fā)(線上資源到sd卡資源的映射),但是不管是白名單還是過(guò)濾靜態(tài)文件類(lèi)型,Native攔截規(guī)則和映射規(guī)則將變得比較復(fù)雜。這里,阿里去啊app的思路就比較贊,我們借鑒一下,將映射規(guī)則交給H5去生成:H5開(kāi)發(fā)完成之后會(huì)掃描H5項(xiàng)目然后生成一份線上資源和離線資源路徑的映射表(souce-router.json),H5容器只需負(fù)責(zé)解析這個(gè)映射表即可。
H5資源包解壓之后在本地的目錄結(jié)構(gòu)類(lèi)似:
$ cd h5 && tree . ├── js/ ├── css/ ├── img/ ├── pages │?? ├── index.html │?? └── list.html └── souce-router.json
souce-router.json的數(shù)據(jù)結(jié)構(gòu)類(lèi)似:
{ "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容器攔截到靜態(tài)資源請(qǐng)求時(shí),如果本地有對(duì)應(yīng)的文件則直接讀取本地文件返回,否則發(fā)起HTTP請(qǐng)求獲取線上資源,如果設(shè)計(jì)完整一點(diǎn)還可以考慮同時(shí)開(kāi)啟新線程去下載這個(gè)資源到本地,下次就走離線了。
下圖演示資源在app內(nèi)部的訪問(wèn)流程圖:
其中proxy指的是開(kāi)發(fā)時(shí)手機(jī)設(shè)置代理http代理到開(kāi)發(fā)機(jī)。
數(shù)據(jù)通道上報(bào)
由于界面由H5和Native共同完成,界面上的用戶交互埋點(diǎn)數(shù)據(jù)最好由H5容器統(tǒng)一采集、上報(bào),還有,由頁(yè)面跳轉(zhuǎn)產(chǎn)生的瀏覽軌跡(轉(zhuǎn)化漏斗),也由H5容器記錄和上報(bào)
ajax代理
因ajax受同源策略限制,可以在hybridApi層對(duì)ajax進(jìn)行統(tǒng)一封裝,同時(shí)兼容H5容器和瀏覽器runtime,采用更高效的通訊通道加速H5的數(shù)據(jù)傳輸
Native對(duì)H5的擴(kuò)展主要指擴(kuò)展H5的硬件接口調(diào)用能力,比如屏幕旋轉(zhuǎn)、攝像頭、麥克風(fēng)、位置服務(wù)等等,將Native的能力通過(guò)接口的形式提供給H5。
綜述最后來(lái)張圖總結(jié)下,hybrid客戶端整體架構(gòu)圖:
其中的Synchronize Service模塊表示和服務(wù)器的長(zhǎng)連接通信模塊,用于接受服務(wù)器端各種推送,包括離線包等。Source Merge Service模塊表示對(duì)解壓后的H5資源進(jìn)行更新,包括增加文件、以舊換新以及刪除過(guò)期文件等。
可以看到,hybrid模式的app架構(gòu),最核心和最難的部分都是H5容器的設(shè)計(jì)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/78409.html
閱讀 2881·2019-08-30 15:44
閱讀 1913·2019-08-29 13:59
閱讀 2851·2019-08-29 12:29
閱讀 1098·2019-08-26 13:57
閱讀 3210·2019-08-26 13:45
閱讀 3342·2019-08-26 10:28
閱讀 857·2019-08-26 10:18
閱讀 1705·2019-08-23 16:52