關於Hybrid模式開發app的好處,網絡上已有不少文章闡述了,這裏不展開。javascript
本文將從如下幾個方面闡述Hybrid app架構設計的一些經驗和思考。css
原文及討論請到 github issuehtml
做爲一種跨語言開發模式,通信層是Hybrid架構首先應該考慮和設計的,日後全部的邏輯都是基於通信層展開。前端
Native(以Android爲例)和H5通信,基本原理:java
Android調用H5:經過webview類的loadUrl
方法能夠直接執行js代碼,相似瀏覽器地址欄輸入一段js同樣的效果git
webview.loadUrl("javascript: alert('hello world')");
H5調用Android:webview能夠攔截H5發起的任意url請求,webview經過約定的規則對攔截到的url進行處理(消費),便可實現H5調用Androidgithub
var ifm = document.createElement('iframe'); ifm.src = 'jsbridge://namespace.method?[...args]';
JSBridge即咱們一般說的橋協議,基本的通信原理很簡單,接下來就是橋協議具體實現。web
P.S:註冊私有協議的作法很常見,咱們常常遇到的在網頁里拉起一個系統app就是採用私有協議實現的。app在安裝完成以後會註冊私有協議到OS,瀏覽器發現自身不能識別的協議(http、https、file等)時,會將連接拋給OS,OS會尋找可識別此協議的app並用該app處理連接。好比在網頁裏以itunes://
開頭的連接是Apple Store的私有協議,點擊後能夠啓動Apple Store而且跳轉到相應的界面。國內軟件開發商也常常這麼作,好比支付寶的私有協議alipay://
,騰訊的tencent://
等等。ajax
因爲JavaScript語言自身的特殊性(單進程),爲了避免阻塞主進程而且保證H5調用的有序性,與Native通信時對於須要獲取結果的接口(GET類),採用相似於JSONP的設計理念:json
類比HTTP的request和response對象,調用方會將調用的api、參數、以及請求籤名(由調用方生成)帶上傳給被調用方,被調用方處理完以後會吧結果以及請求籤名回傳調用方,調用方再根據請求籤名找到本次請求對應的回調函數並執行,至此完成了一次通信閉環。
H5調用Native(以Android爲例)示意圖:
Native(以Android爲例)調用H5示意圖:
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都能完成界面開發。幾乎全部hybrid的開發模式都會碰到一樣的一個問題:哪些由Native負責哪些由H5負責?
這個回到原始的問題上來:咱們爲何要採用hybrid模式開發?簡而言之就是同時利用H5的跨平臺、快速迭代能力以及Native的流暢性、系統API調用能力。
根據這個原則,爲了充分利用兩者的優點,應該儘量地將app內容使用H5來呈現,而對於js語言自己的缺陷,應該使用Native語言來彌補,如轉場動畫、多線程做業(密集型任務)、IO性能等。即總的原則是H5提供內容,Native提供容器,在有可能的條件下對Android原生webview進行優化和改造(參考阿里Hybrid容器的JSM),提高H5的渲染效率。
可是,在實際的項目中,將整個app全部界面都使用H5來開發也有不妥之處,根據經驗,如下情形仍是使用Native界面爲好:
因H5比較容易被惡意攻擊,對於安全性要求比較高的界面,如註冊界面、登錄、支付等界面,會採用Native來取代H5開發,保證數據的安全性,這些頁面一般UI變動的頻率也不高。
對於這些界面,降級的方案也有,就是HTTPS。可是想說的是在國內的若網絡環境下,HTTPS的體驗實在是不咋地(主要是慢),並且只能走現網不能走離線通道。
另外,H5自己的動畫開發成本比較高,在低端機器上可能有些繞不過的性能坎,原生js對於手勢的支持也比較弱,所以對於這些類型的界面,能夠選擇使用Native來實現,這也是Native自己的優點不是。好比要實現下面這個音樂播放界面,用H5開發門檻不小吧,留意下中間的波浪線背景,手指左右滑動能夠切換動畫。
導航組件,就是頁面的頭組件,左上角通常都是一個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組件,如alert、toast等將採用Native來實現:
H5自己有這些組件,可是一般比較簡陋,不能和APP UI風格統一,須要再定製,好比alert組件背景增長遮罩層
H5來實現這些組件有時會存在座標、尺寸計算偏差,好比筆者以前遇到的是頁面load異常須要調用對話框組件提示,可是這時候頁面高度爲0,因此會出現彈窗「消失」的現象
這些組件一般功能單一可是通用,適合作成公用組件整合到HybridApi裏邊
下面代碼演示H5調用Native提供的UI組件:
JSBridge.ui.toast('Hello world!');
因爲H5是在H5容器裏進行加載和渲染,因此Native很容易對H5頁面的行爲進行監控,包括進度條、loading動畫、404監控、5xx監控、網絡診斷等,而且在H5加載異常時提供默認界面供用戶操做,防止APP「假死」。
下面是微信的5xx界面示意:
Native除了負責部分界面開發和公共UI組件設計以外,做爲H5的runtime,H5容器是hybrid架構的核心部分,爲了讓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資源放置到本地離線訪問,最大的挑戰就是本地資源的動態更新如何設計,這部分能夠說是最複雜的了,由於這同時涉及到H五、Native和服務器三方,覆蓋式離線更新示意圖以下:
解釋下上圖,開發階段H5代碼能夠經過手機設置HTTP代理方式直接訪問開發機。完成開發以後,將H5代碼推送到管理平臺進行構建、打包,而後管理平臺再經過事先設計好的長鏈接通道將H5新版本信息推送給客戶端,客戶端收到更新指令後開始下載新包、對包進行完整性校驗、merge回本地對應的包,更新結束。
其中,管理平臺推送給客戶端的信息主要包括項目名(包名)、版本號、更新策略(增量or全量)、包CDN地址、MD5等。
一般來講,H5資源分爲兩種,常常更新的業務代碼和不常常更新的框架、庫代碼和公用組件代碼,爲了實現離線資源的共享,在H5打包時能夠採用分包的策略,將公用部分單獨打包,在本地也是單獨存放,分包及合併示意圖:
離線資源更新的問題解決了,剩下的就是如何使用離線資源了。
上面已經提到,對於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的數據傳輸
主要指擴展H5的硬件接口調用能力,好比屏幕旋轉、攝像頭、麥克風、位置服務等等,將Native的能力經過接口的形式提供給H5。
最後來張圖總結下,hybrid客戶端總體架構圖:
其中的Synchronize Service
模塊表示和服務器的長鏈接通訊模塊,用於接受服務器端各類推送,包括離線包等。Source Merge Service
模塊表示對解壓後的H5資源進行更新,包括增長文件、以舊換新以及刪除過時文件等。
能夠看到,hybrid模式的app架構,最核心和最難的部分都是H5容器的設計。