本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1275&extra=page%3D1javascript
跨平臺,是H5最重要的能力之一。而 Hybrid H5 因強依賴於具體 app,每每不具備跨平臺性。這時,將強依賴關係解耦,便可恢復 H5 的跨平臺能力。近期本人負責 手Q 紅包打賞項目的前端開發,因項目涉及到多 app 跨平臺兼容,對 hybrid H5 的跨平臺性有了必定的感悟和思考。在這裏作下總結分享,但願能對你們有所收穫。php
進入正題以前,先解釋下本文主題的兩個名詞:css
①Hybrid H5,即混合了原生能力的 H5。區別於純粹 web 端的 H5,它可調用原生的能力,強依賴於具體原生 app,與原生共同構建整個 app 的 UI 層,是 app UI 層很好的靈活性補充。微信和 手Q 上的 H5 業務通常都屬於 Hybrid H5 的範疇。html
② 跨平臺性,即一個 H5 頁面可同時運行在多個平臺上。可運行平臺越多,跨平臺性就越強。在現在移動互聯網的發展大潮中,H5 能與體驗更優的原生終端齊步並進,其跨平臺性可謂功不可沒。前端
因強依賴於具體 app,Hybrid H5 每每不具備跨平臺性。java
本文將從 Hybrid H5 與原生的通信原理出發,逐步探討如何經過解耦來恢復 Hybrid H5 的跨平臺性。android
原理圖ios
從原理圖中,有4個關鍵點:web
1個通信媒介——原生自定義的通信協議;json
以及圍繞着通信媒介執行的3個通信行爲——觸發、調用、回調。
關鍵點詳解
①通信媒介——原生通信協議:原生自定義的僞協議,通常會定義成與 http 協議相似的格式:
協議名://接口路徑?參數1=XXX&參數2=XXX&參數3=XXX#callback
其中:
a、協議名:app 自定義的協議名,用於H5觸發行爲的監控捕獲,如 手Q 使用的 jsbridge://;<br> b、接口路徑:原生具體能力路徑,不一樣原生能力路徑不一樣;<br> c、參數1=XXX&參數2=XXX&參數3=XXX#callback:H5傳參與回調方法標識;
根據通信協議規範,便可針對不一樣的原生能力給H5提供不一樣的調用地址,如:
jsbridge://method?a=2&b=3#h5MethodTag
**②通信行爲——觸發:**能被原生監聽並捕獲截攔的H5行爲,均可以做爲原生通信協議的觸發行爲。
Hybrid H5 的這類行爲有 console.log、alert、confirm、prompt、location.href 等。將原生協議內容經過其中的某一行爲觸發,便可被原生正確捕獲並解析。如:
location.href ='jsbridge://method?a=2&b=3#h5MethodTag'
H5調用後,原生終端會捕獲到內容:jsbridge://method?a=2&b=3#h5MethodTag
**③通信行爲——調用:**原生終端根據 H5 傳過來的內容,解析匹配後會路由到具體處理方法,執行原生能力邏輯。以 ios 爲例(swift 語言),「調用」邏輯以下:
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) - Bool { let url = request.URL //url let scheme = url ? .scheme //協議名 let method = url ? .host //接口路徑 let query = url ? .query //參數 if url != nil && scheme == "jsbridge" { /*根據method路由*/ switch method!{ case "method": self.method() case "openTenpayView": self.openTenpayView() ...其餘方法... default: } return false } else { return true } }
原生終端根據捕獲到的協議內容,進行解析獲取,若僞協議爲原生指定的僞協議(「jsbridge"),就會根據 method 內容和 query 參數進行路由操做,尋找具體的方法執行邏輯。不然,忽略處理,按照 webview 原有跳轉邏輯處理。以第②步觸發的僞協議內容爲例,在本例「調用」代碼中被原生捕獲後,會路由執行邏輯:self.method();
**④通信行爲——回調:**原生根據 H5 傳過來的內容,捕獲 js 回調函數方法名,在原生邏輯執行結束後,將執行結果帶到回調函數中並執行 js 回調函數。經過在第③步「調用」執行完後,ios 會調用 js 回調函數 H5MethodTag:
/*解析到H5的回調函數名爲H5MethodTag(#號後內容),回調執行js的方法*/ webview.stringByEvaluatingJavaScriptFromString("H5MethodTag(data)")
經過以上4個關鍵點,便可作到 H5 與原生終端的相互通信,完成H5對原生能力的調用。
由上述通信原理了解到,Hybrid H5 直接調用定義好的原生通信協議,便可完成通信全過程。但這裏有一個明顯的問題,即 Hybrid H5 會強耦合於當前平臺。不說跨 app 了,app 內跨平臺(android/ios/wp)都會顯示吃力。這裏面有不少緣由,其中一個較明顯的緣由在於,不一樣平臺 app 開發團隊通信協議規範定義存在不一致。再者,H5 業務代碼上滿滿的相似 jsonp 的協議調用,也並很差維護。
要達到 Hybrid H5 在 app 內跨平臺,業界常見作法是 app 對外提供 jsapi。經過 jsapi 將各平臺協議規範差別進行封裝,解耦通信邏輯,並以函數接口的方式提供給 Hybrid H5 調用。jsapi 接口通常會定義成以下格式:
ns.method({ /*cfg參數對象*/ }, function(data) { /*回調*/ })
原理圖
**原理核心:**H5 與原生通信之間增長一層 jsapi,jsapi 完成三大行爲:api 接口建立、協議 url 組裝、建立 iframe 發起僞協議請求;
因 手Q 的 jsapi 相對比較成熟,下面會以 手Q jsapi 中的核心源碼進行分析。
**①api接口建立:**js 函數接口封裝、平臺差別處理,方便H5函數調用
mqq.build('mqq.tenpay.openTenpayView', { iOS: function(options, callback) { var callbackName = callback ? mqq.callback(callback) : null; mqq.invokeClient('pay', 'openTenpayView', { 'params': options, 'callback': callbackName }); }, android: function(params, callback) { mqq.invokeClient('pay', 'openTenpayView', JSON.stringify(params), callback); }, supportInvoke: true, support: { iOS: '4.6.1', android: '4.6.1' } });
mqq.build 方法爲 api 接口建立方法。經過傳入待建立的 jsapi 方法名(mqq.tenpay.openTenpayView)和不一樣平臺(android/ios)的差別處理配置。最終會生成H5可調用方法:
mqq.tenpay.openTenpayView({ /*data*/ },function(ret){ /*callback*/ })
**②協議url組裝:**從接口到 url 協議的轉換、回調處理,完成協議 url 建立
第①步中,不一樣平臺差別處理都會調用 mqq.invokeClient 方法,該方法實際處理的就是原理圖中與原生通信的過程。咱們先來看協議 url 組裝的過程。
/*生成回調索引*/ sn = storeCallback(callback); /*協議路徑組裝*/ url = 'jsbridge://' + encodeURIComponent(ns) + '/' + encodeURIComponent(method); /*參數組裝*/ argus.forEach(function(a, i) { if (exports.isObject(a)) { a = JSON.stringify(a); } if (i === 0) { url += '?p='; } else { url += '&p' + i + '='; } url += encodeURIComponent(String(a)); }); /*回調函數索引組裝*/ url += '#' + sn; /*連接調用*/ result = openURL(url, ns, method);
協議 url 組裝的過程其實是對傳入參數按協議規範進行拼串的過程,其中包括匿名回調函數的回調索引建立、協議名&協議路徑拼串、傳參循環遍歷拼串。
**③建立 iframe 發起僞協議請求:**請求觸發
/*建立隱藏iframe*/ var iframe = document.createElement('iframe'); iframe.style.cssText = 'display:none;width:0px;height:0px;'; function failCallback() { /*錯誤處理*/ } /*iframe協議調用*/ iframe.onload = failCallback; iframe.src = url; (document.body || document.documentElement).appendChild(iframe); /*刪除iframe*/ setTimeout(function() { iframe && iframe.parentNode && iframe.parentNode.removeChild(iframe); }, 0);
經過建立 iframe 來完成協議調用,並在調用結束後將 iframe 刪除,便可在不影響原 H5 流程的狀況下完成調用全過程。
經過上述的解耦處理,Hybrid H5 已經能夠在 app 內各平臺運行了。但每每這種 jsapi 是 app 級提供的 jsapi(下面簡稱 app jsapi),app jsapi 並不會去兼容別的 app 的差別。而實際狀況具體到某一 Hybrid H5,尤爲是與 app 外部合做的 Hybrid H5,則並不只僅只運行在一個 app上。好比信用卡還款業務,微信有,手Q 也有,功能都同樣。這種狀況就須要進一步的解耦,從業務側再抽離一層 jsapi(下面簡稱 H5 jsapi)來處理 app 間的差別,而非每一個 app 各自一套 H5。
原理圖
**原理核心:**Hybrid H5 業務上增長多一層自維護的 H5 jsapi,H5 jsapi 完成兩大行爲:app jsapi 差別請求、app jsapi 差別封裝。
**①app jsapi 差別請求:**根據當前運行環境 app 請求具體的 app jsapi
下面以 Hybrid H5 需同時運行在手Q和空間獨立版的 app jsapi 差別請求處理邏輯。
<script type="text/javascript" > (function() { var ua = navigator.userAgent || "", isQQ = ua.match(/QQ\/([\d\.]+)/), isQzone = ua.match("Qzone"); if (isQQ) { document.write("<script src='https://open.mobile.qq.com/sdk/qqapi.js?_bid=152'><\x2Fscript>"); } else if (isQzone) { document.write("<script src='https://qzonestyle.gtimg.cn/qzone/phone/m/v4/widget/mobile/jsbridge.js'><\x2Fscript>"); } else { // 不是已兼容app,跳轉到兼容app上運行 var currentHref = window.location.href; /*跳轉到手Q打開本頁面*/ window.location.href = 'mqqapi://forward/url?url_prefix=' + btoa(currentHref) + '&version=1&src_type=web'; /*該頁面支持自定義彈層*/ setTimeout(function() { var _tempBox = confirm('請在手機QQ中使用~'); if (_tempBox == true) { /*跳至手Q打開*/ window.location.href = 'mqqapi://forward/url?url_prefix=' + btoa(currentHref) + '&version=1&src_type=web'; } }, 0) } })() </script>
除了對需兼容的 app 進行差別請求外,還應對在不兼容的 app 運行時作跳轉到主兼容 app 打開當前頁面的邏輯處理,並作引導性提示,保障頁面的完整可用性。
**②app jsapi 差別封裝:**根據當前具體運行的平臺調用相應的 app jsapi 接口。H5 jsapi 的接口形式儘可能與主運行 app 的 jsapi 保持一致
下面以打開 QQ 錢包原生頁和原生頁面跳轉能力爲例,作 app jsapi 的差別封裝。
var mod = { ... openTenpayView: function(param, callback) { if (isQQ) { var param = $.extend({ userId: $.getCookie('uin').replace(/^o0*/, '') }, param); mqq.tenpay.openTenpayView(param, callback); } else { /*調起手Q打開中轉頁jump.html,由中轉頁打開原生功能頁*/ var targetHref = 'http://testhost.com/jump.html?go=' + param.viewTag + '&_wv=' + (1 + 2 + 1024 + 2097152 + 33554432); //跳轉url /*跳到手Q*/ window.location.href = 'mqqapi://forward/url?url_prefix=' + btoa(targetHref) + '&version=1&src_type=web'; } }, openUrl: function(paramObj) { if (isQQ) { mqq.ui.openUrl({ url: paramObj.url, target: 1 }); } else if (isQzone) { mqq.invoke("ui", "openUrl", { url: paramObj.url, target: 1, style: 1 }); } else { /*兼容處理*/ location.href = paramObj.url } }, ...其餘接口... }; return mod;
調用 openTenpayView,頁面能在 手Q 中正常調用,而在非 手Q 時則跳轉回 手Q 打開處理;
調用 openUrl,對於 手Q 和空間獨立版作相應的接口調用,而其餘平臺則直接使用 H5 的 location 跳轉。
H5 本質是具備跨平臺性的。Hybrid H5 因混合了原生能力,強耦合於原生,再也不具備跨平臺性。要恢復其跨平臺能力,關鍵在解耦,將其耦合於原生的部分解耦封裝起來。
解耦是開發很重要的一項能力,Hybrid H5 跨平臺性的迴歸正得益於解耦的處理。
因耦合而致使某項能力減弱的狀況還有不少,好比 H5 的靈活性等等。遇到這種狀況你們不妨也試試解耦,或許會收到意想不到的驚喜。
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!