JsBridge是Hybrid項(xiàng)目中一種實(shí)現(xiàn)H5與Native兩者之間通訊的成熟、安全的解決方案。
基礎(chǔ)原理
JsBridge摹量,顧名思義,它是一座橋谒所,架在H5和Native之間的橋;通過(guò)這座橋沛申,H5這邊可以調(diào)用原生的方法劣领,比如調(diào)用Native的分享、相機(jī)等等這些H5無(wú)法很好滿足的功能污它;同時(shí),Native也可以通過(guò)這座橋德澈,更為方便固惯、規(guī)范地使用約定好的方法。
那JsBridge的通訊機(jī)理是什么呢屡穗?其實(shí)就是url scheme的觸發(fā)與攔截村砂。
H5調(diào)用Native
JsBridge中定義好一套u(yù)rl scheme協(xié)議屹逛,H5端根據(jù)協(xié)議觸發(fā)響應(yīng)的url罕模,頁(yè)面載體webview攔截住url后淑掌,解析發(fā)現(xiàn)是約定好的通訊協(xié)議抛腕,Native根據(jù)解析結(jié)果調(diào)用響應(yīng)的原生方法,進(jìn)行響應(yīng)的操作侥钳。
Native調(diào)用H5
那么柄错,Native是怎么調(diào)用H5中的方法的呢苦酱?不好意思,是Native創(chuàng)建了H5的載體webview颂跨,所以Native是爸爸扯饶,它可以直接調(diào)用頁(yè)面中的全局函數(shù)方法。但是既然使用了JsBridge钓丰,那么肯定是要按照協(xié)議執(zhí)行的携丁,“執(zhí)法辦事”是必要的梦鉴,不然代碼沒(méi)有規(guī)范肥橙,難以落成文檔存筏,后期迭代維護(hù)成本會(huì)遞增方篮。
在使用JsBridge的情況下藕溅,JsBridge將會(huì)掛載在頁(yè)面的window變量中,H5將Native所需調(diào)用的方法注冊(cè)到JsBridge中汁掠;Native然后根據(jù)約定的方式對(duì)這些方法進(jìn)行傳參和調(diào)用,具體實(shí)現(xiàn)會(huì)在下面源碼解析板塊進(jìn)行解讀集币。
源碼剖析
JsBridge的源碼不多,不到200行鞠苟,下面我們按照“自己手寫(xiě)一個(gè)JsBridge”的思路來(lái)對(duì)源碼進(jìn)行搜索拆分。
H5調(diào)用Native
根據(jù)事先機(jī)理当娱,我首先想到的是吃既,把實(shí)現(xiàn)H5調(diào)用Native的代碼捋出來(lái)跨细。那么鹦倚,我們需要在頁(yè)面中來(lái)觸發(fā)url scheme。我們會(huì)想到window.location.href冀惭、a標(biāo)簽或者iframe等等震叙。源碼中使用的是iframe.src來(lái)觸發(fā),為什么呢媒楼?因?yàn)閣indow.location.href和a標(biāo)簽其實(shí)是一樣,其如果短時(shí)間內(nèi)觸發(fā)多次匣砖,webview只會(huì)捕獲最后一次請(qǐng)求而忽略之前的
,這個(gè)解釋援引搜索內(nèi)容拂共,我沒(méi)有去進(jìn)行驗(yàn)證,有興趣的同學(xué)可以驗(yàn)證下哦宜狐。下面先看第一段代碼咱台,描述我就直接寫(xiě)在在代碼的注釋中了。
var messagingIframe; // 觸發(fā)url scheme的iframe
var bizMessagingIframe; // 又定義了一個(gè)俭驮,這個(gè)我們不管回溺,再往下看看
var sendMessageQueue = []; // 存放H5向Native發(fā)送的消息隊(duì)列
var receiveMessageQueue = []; // 存放Native發(fā)送給H5的消息
var messageHandlers = {}; // 可供native調(diào)用的方法,通過(guò)registerHandler注冊(cè)存入messageHandlers
var CUSTOM_PROTOCOL_SCHEME = 'yy'; // url scheme中用以標(biāo)志協(xié)議的字段
var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; // url scheme中另一個(gè)字段
var responseCallbacks = {}; // 存放callback的對(duì)象
var uniqueId = 1; // 用于生成callbackId的標(biāo)志之一混萝,每次生成后都會(huì)加1
有了進(jìn)行通訊的url遗遵,我們下面就來(lái)看看是怎么進(jìn)行觸發(fā)的吧。掛載window上的WebViewJavascriptBridge中定義一些方法逸嘀,callHandler就是用來(lái)給H5調(diào)用Native使用的车要。
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
init: init, // 初始化
send: send, // 單純的H5發(fā)送消息
registerHandler: registerHandler, // 用以注冊(cè)Native需要調(diào)用的js方法
callHandler: callHandler, // 發(fā)送消息,并指明需要調(diào)用的Native方法
_fetchQueue: _fetchQueue, // Native調(diào)用崭倘,用以獲取sendMessageQueue中的消息
_handleMessageFromNative: _handleMessageFromNative // Native調(diào)用翼岁,給H5發(fā)消息
};
callHandler只是對(duì)_doSend函數(shù)的簡(jiǎn)單封裝,看來(lái)具體的實(shí)現(xiàn)是在_doSend中
function callHandler(handlerName, data, responseCallback) {
_doSend({ // 發(fā)送的message包含調(diào)用的native方法名稱(chēng)以及傳輸?shù)臄?shù)據(jù)
handlerName: handlerName,
data: data
}, responseCallback);
}
/**
* [_doSend H5調(diào)用native,對(duì)callHandler的豐富]
* @param {[type]} message [調(diào)用消息體]
* @param {[type]} responseCallback [回調(diào)函數(shù)]
* @constructor
* @return {[type]} [description]
*/
function _doSend(message, responseCallback) {
// 如果傳入回調(diào)函數(shù)司光,則為回調(diào)函數(shù)生成一個(gè)callbackId登澜,
// 以callbackId為key,將回調(diào)函數(shù)存入responseCallbacks對(duì)象中,用于之后回調(diào)使用
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); // callbackId
responseCallbacks[callbackId] = responseCallback; // 將callbackId維護(hù)字在responseCallbacks中
message.callbackId = callbackId; // message新加一個(gè)callbackId字段
// 當(dāng)前message包含內(nèi)容
// {
// callbackId: callbackId, // 回調(diào)函數(shù)id
// handlerName: handlerName, // 調(diào)用的Native方法名
// data: data // 傳參數(shù)據(jù)
// }
}
sendMessageQueue.push(message);
// 通過(guò)iframe通知客戶端有消息了
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
這邊我們便可以來(lái)解釋下最初為啥要?jiǎng)?chuàng)建兩個(gè)iframe。messagingIframe并不是用來(lái)傳輸消息的飘庄,而只是告訴Native有消息需要處理了,真正用來(lái)傳輸消息的iframe是bizMessagingIframe购撼。
// 提供給native調(diào)用,該函數(shù)作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內(nèi)容,所以使用url shouldOverrideUrlLoading 的方式返回內(nèi)容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
Native得到通知后跪削,通過(guò)_fetchQueue方法將sendMessageQueue中消息取出并通過(guò)bizMessagingIframe觸發(fā)url scheme來(lái)傳輸消息。說(shuō)實(shí)話迂求,這邊的實(shí)現(xiàn)有點(diǎn)迷糊碾盐,感覺(jué)沒(méi)必要多中間通知這個(gè)環(huán)節(jié),可能作者另有考慮吧揩局。
Native調(diào)用H5
WebViewJavascriptBridge中定義的_handleMessageFromNative就是用來(lái)處理Native對(duì)H5的調(diào)用
// Native調(diào)用H5主要分為兩種
// 1毫玖、H5調(diào)用Native時(shí),傳入的callback,Native執(zhí)行完后付枫,現(xiàn)在來(lái)執(zhí)行回調(diào)
// 2烹玉、Native調(diào)用H5方法,可以選擇傳入需要回調(diào)的Native函數(shù)
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() { // 異步掛起處理阐滩,不會(huì)影響同步任務(wù)
var message = JSON.parse(messageJSON);
var responseCallback;
if (message.responseId) { // 如果消息中有responseId二打,代表這是native執(zhí)行完,這個(gè)responseId其實(shí)就是_doSend函數(shù)中傳入的callbackId
responseCallback = responseCallbacks[message.responseId]; // 根據(jù)responseId取出之前存入responseCallbacks對(duì)象中的回調(diào)函數(shù)
if (!responseCallback) {
return;
}
responseCallback(message.responseData); // 傳入message.responseData參數(shù)掂榔,執(zhí)行callback
delete responseCallbacks[message.responseId]; // callback執(zhí)行完后继效,從responseCallbacks隊(duì)列中刪除該callback
} else { // native調(diào)用H5的方法
// Native調(diào)用H5方法過(guò)程與之前類(lèi)似,也可以要求有個(gè)回調(diào)
if (message.callbackId) { // callbackId代表装获,native要求H5方法執(zhí)行完后瑞信,給native一個(gè)回調(diào)
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId, // responseId告訴native這是你要的回調(diào)
responseData: responseData
});
};
}
var handler = WebViewJavascriptBridge._messageHandler; // init時(shí)定義的H5默認(rèn)方法,如果native沒(méi)有指明handlerName的情況下穴豫,就會(huì)調(diào)用默認(rèn)方法
if (message.handlerName) { // native指定了要調(diào)用H5方法
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
至此凡简,JsBridge中H5與Native之間通訊的主體實(shí)現(xiàn)代碼就已經(jīng)講完了。還是蠻簡(jiǎn)單的绩郎,其實(shí)就是定義了兩者通訊的一種協(xié)議潘鲫。
最后
年后本想著寫(xiě)一篇文章來(lái)闡述Hybrid實(shí)現(xiàn)原理的完整技術(shù)解讀,奈何在原生部分有短板肋杖,就只能寫(xiě)個(gè)JsBridge來(lái)個(gè)源碼解析來(lái)解解饞了~