【騰訊bugly乾貨分享】解耦---Hybrid H5跨平臺性思考

本文來自於騰訊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

Hybrid H5 跨平臺性

進入正題以前,先解釋下本文主題的兩個名詞: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

Hybrid H5 與原生的通信原理

原理圖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對原生能力的調用。

初次解耦:app 內跨平臺——建立jsapi解耦通信邏輯、封裝平臺差別

由上述通信原理了解到,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 流程的狀況下完成調用全過程。

再次解耦:app 間跨平臺—— jsapi 細化,封裝 app 差別

經過上述的解耦處理,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 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索