一文搞懂jsBridge的執行機制
我司的APP是一個典型的混合開發APP,內嵌的都是前端頁面,前端頁面要做到和原生的效果相似,就避免不了呼叫一些原生的方法,jsBridge
就是js
和原生
通訊的橋樑,本文不講概念性的東西,而是通過分析一下我司專案中的jsBridge
原始碼,來從前端角度大概瞭解一下它是怎麼實現的。
js呼叫方式
先來看一下,js
是怎麼來呼叫某個原生方法的,首先初始化的時候會呼叫window.WebViewJavascriptBridge.init
方法:
window.WebViewJavascriptBridge.init()
然後如果要呼叫某個原生方法可以使用下面的函式:
function native (funcName, args = {}, callbackFunc, errorCallbackFunc) { // 校驗引數是否合法 if (args && typeof args === 'object' && Object.prototype.toString.call(args).toLowerCase() === '[object object]' && !args.length) { args = JSON.stringify(args); } else { throw new Error('args不符合規範'); } // 判斷是否是手機環境 if (getIsMobile()) { // 呼叫window.WebViewJavascriptBridge物件的callHandler方法 window.WebViewJavascriptBridge.callHandler( funcName, args, (res) => { res = JSON.parse(res); if (res.code === 0) { return callbackFunc(res); } else { return errorCallbackFunc(res); } } ); } }
傳入要呼叫的方法名、引數和回撥即可,它先校驗了一下引數,然後會呼叫window.WebViewJavascriptBridge.callHandler
方法。
此外也可以提供回撥供原生呼叫:
window.WebViewJavascriptBridge.registerHandler(funcName, callbackFunc);
接下來看一下window.WebViewJavascriptBridge
物件到底是啥。
安卓
WebViewJavascriptBridge.js
檔案內是一個自執行函式,首先定義了一些變數:
// 定義變數 var messagingIframe; var sendMessageQueue = [];// 傳送訊息的佇列 var receiveMessageQueue = [];// 接收訊息的佇列 var messageHandlers = {};// 訊息處理器 var CUSTOM_PROTOCOL_SCHEME = 'yy';// 自定義協議 var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; var responseCallbacks = {};// 響應的回撥 var uniqueId = 1;
根據變數名簡單翻譯了一下,具體用處接下來會分析。接下來定義了WebViewJavascriptBridge
物件:
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative };
可以看到就是一個普通的物件,上面掛載了一些方法,具體方法暫時不看,繼續往下:
var doc = document;
_createQueueReadyIframe(doc);
呼叫了_createQueueReadyIframe
方法:
function _createQueueReadyIframe (doc) {
messagingIframe = doc.createElement('iframe');
messagingIframe.style.display = 'none';
doc.documentElement.appendChild(messagingIframe);
}
這個方法很簡單,就是建立了一個隱藏的iframe
插入到頁面,繼續往下:
// 建立一個Events型別(基礎事件模組)的事件(Event)物件
var readyEvent = doc.createEvent('Events');
// 定義事件名為WebViewJavascriptBridgeReady
readyEvent.initEvent('WebViewJavascriptBridgeReady');
// 通過document來觸發該事件
doc.dispatchEvent(readyEvent);
這裡定義了一個自定義事件,並直接派發了,其他地方可以像通過監聽原生事件一樣監聽該事件:
document.addEventListener(
'WebViewJavascriptBridgeReady',
function () {
console.log(window.WebViewJavascriptBridge)
},
false
);
這裡的用處我理解就是當該jsBridge
檔案如果是在其他程式碼之後引入的話需要保證之前的程式碼能知道window.WebViewJavascriptBridge
物件何時可用,如果規定該jsBridge
必須要最先引入的話那麼就不需要這個處理了。
到這裡自執行函式就結束了,接下來看一下最開始的init
方法:
function init (messageHandler) {
if (WebViewJavascriptBridge._messageHandler) {
throw new Error('WebViewJavascriptBridge.init called twice');
}
// init呼叫的時候沒有傳參,所以messageHandler=undefined
WebViewJavascriptBridge._messageHandler = messageHandler;
// 當前receiveMessageQueue也只是一個空陣列
var receivedMessages = receiveMessageQueue;
receiveMessageQueue = null;
for (var i = 0; i < receivedMessages.length; i++) {
_dispatchMessageFromNative(receivedMessages[i]);
}
}
從初始化的角度來看,這個init
方法似乎啥也沒做。接下來我們來看callHandler
方法,看看是如何呼叫安卓的方法的:
function callHandler (handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
處理了一下引數又呼叫了_doSend
方法:
function _doSend (message, responseCallback) {
// 如果提供了回撥的話
if (responseCallback) {
// 生成一個唯一的回撥id
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
// 回撥通過id儲存到responseCallbacks物件上
responseCallbacks[callbackId] = responseCallback;
// 把該回調id新增到要傳送給native的訊息裡
message.callbackId = callbackId;
}
// 訊息新增到訊息佇列裡
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
這個方法首先把呼叫原生方法時的回撥函式通過生成一個唯一的id
儲存到最開始定義的responseCallbacks
物件裡,然後把該id
新增到要傳送的資訊上,所以一個message
的結構是這樣的:
{
handlerName,
data,
callbackId
}
接著把該message
新增到最開始定義的sendMessageQueue
數組裡,最後設定了iframe
的src
屬性:yy://__QUEUE_MESSAGE__/
,這其實就是一個自定義協議的url
,我簡單搜尋了一下,native
會攔截這個url
來做相應的處理,到這裡我們就走不下去了,因為不知道原生做了什麼事情,簡單搜尋了一下,發現了這個庫:WebViewJavascriptBridge,我司應該是在這個庫基礎上修改的,結合了網上的一些文章後大概知道了,原生攔截到這個url
後會呼叫js
的window.WebViewJavascriptBridge._fetchQueue
方法:
function _fetchQueue () {
// 把我們要傳送的訊息佇列轉成字串
var messageQueueString = JSON.stringify(sendMessageQueue);
// 清空訊息佇列
sendMessageQueue = [];
// 安卓無法直接讀取返回的資料,因此還是通過iframe的src和java通訊
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
安卓攔截到url
後,知道js
給安卓傳送訊息了,所以主動呼叫js
的_fetchQueue
方法,取出之前新增到佇列裡的訊息,因為無法直接讀取js
方法返回的資料,所以把格式化後的訊息新增到url
上,再次通過iframe
來發送,此時原生又會攔截到yy://return/_fetchQueue/
這個url
,那麼取出後面的訊息,解析出要其中要執行的原生方法名和引數後執行對應的原生方法,當原生方法執行完後又會主動呼叫js
的window.WebViewJavascriptBridge._handleMessageFromNative
方法:
function _handleMessageFromNative (messageJSON) {
// 根據之前的init方法的邏輯我們知道receiveMessageQueue是會被設定為null的,所以會走else分支
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
看一下_dispatchMessageFromNative
方法做了什麼:
function _dispatchMessageFromNative (messageJSON) {
setTimeout(function () {
// 原生髮回的訊息是字串型別的,轉成json
var message = JSON.parse(messageJSON);
var responseCallback;
// java呼叫完成,發回的responseId就是我們之前傳送給它的callbackId
if (message.responseId) {
// 從responseCallbacks物件裡取出該id關聯的回撥方法
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
// 執行回撥,js呼叫安卓方法後到這裡順利收到訊息
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// ...
}
});
}
messageJSON
就是原生髮回的訊息,裡面除了執行完原生方法後返回的相關資訊外,還帶著之前我們傳給它的callbackId
,所以我們可以通過這個id
來在responseCallbacks
裡找到關聯的回撥並執行,本次js
呼叫原生方法流程結束。但是,明顯函式裡還有不存在id
時的分支,這裡是用來幹啥的呢,我們前面介紹的都是js
呼叫原生方法,但是顯然,原生也可以直接給js
發訊息,比如常見的攔截返回鍵功能,當原生監聽到返回鍵事件後它會主動傳送資訊告訴前端頁面,頁面就可以執行對應的邏輯,這個else
分支就是用來處理這種情況:
function _dispatchMessageFromNative (messageJSON) {
setTimeout(function () {
if (message.responseId) {
// ...
} else {
// 和我們傳給原生的訊息可以帶id一樣,原生傳給我們的訊息也可以帶一個id,同時原生內部也會通過這個id關聯一個回撥
if (message.callbackId) {
var callbackResponseId = message.callbackId;
//如果前端需要再給原生回訊息的話那麼就帶上原生之前傳來的id,這樣原生就可以通過id找到對應的回撥並執行
responseCallback = function (responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
// 我們並沒有設定預設的_messageHandler,所以是undefined
var handler = WebViewJavascriptBridge._messageHandler;
// 原生髮送的訊息裡面有處理方法名稱
if (message.handlerName) {
// 通過方法名稱去messageHandlers物件裡查詢是否有對應的處理方法
handler = messageHandlers[message.handlerName];
}
try {
// 執行處理方法
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console !== 'undefined') {
console.log('WebViewJavascriptBridge: WARNING: javascript handler threw.', message, exception);
}
}
}
});
}
比如我們要監聽原生的返回鍵事件,我們先通過window.WebViewJavascriptBridge
物件的方法註冊一下:
window.WebViewJavascriptBridge.registerHandler('onBackPressed', () => {
// 做點什麼...
})
registerHandler
方法如下:
function registerHandler (handlerName, handler) {
messageHandlers[handlerName] = handler;
}
很簡單,把我們要監聽的事件名和方法都儲存到messageHandlers
物件上,然後如果原生監聽到返回鍵事件後會傳送如下結構的訊息:
{
handlerName: 'onBackPressed'
}
這樣就可以通過handlerName
找到我們註冊的函式進行執行了。
到此,安卓環境的js
和原生互相呼叫的邏輯就結束了,總結一下就是:
1.js
呼叫原生
生成一個唯一的id
,把回撥和id
儲存起來,然後將要傳送的資訊(帶上本次生成的唯一id)新增到一個佇列裡,之後通過iframe
傳送一個自定義協議的請求,原生攔截到後呼叫js
的window.WebViewJavascriptBridge
物件的一個方法來獲取佇列的資訊,解析出請求和引數後執行對應的原生方法,然後再把響應(帶上前端傳來的id)通過呼叫js
的window.WebViewJavascriptBridge
的指定方法傳遞給前端,前端再通過id
找到之前儲存的回撥,進行執行。
2.原生呼叫js
首先前端需要事先註冊要監聽的事件,把事件名和回撥儲存起來,然後原生在某個時刻會呼叫js
的window.WebViewJavascriptBridge
物件的指定方法,前端根據返回引數的事件名找到註冊的回撥進行執行,同時原生也會傳過來一個id
,如果前端執行完相應邏輯後還要給原生回訊息,那麼要把該id
帶回去,原生根據該id
來找到對應的回撥進行執行。
可以看到,js
和原生兩邊的邏輯都是一致的。
ios
ios
和安卓基本是一致的,部分細節上有點區別,首先是協議不一樣,ios
的是這樣的:
var CUSTOM_PROTOCOL_SCHEME_IOS = 'https';
var QUEUE_HAS_MESSAGE_IOS = '__wvjb_queue_message__';
然後ios
初始化建立iframe
的時候會發送一個請求:
var BRIDGE_LOADED_IOS = '__bridge_loaded__';
function _createQueueReadyIframe (doc) {
messagingIframe = doc.createElement('iframe');
messagingIframe.style.display = 'none';
if (isIphone()) {
// 這裡應該是ios需要先載入一下bridge
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME_IOS + '://' + BRIDGE_LOADED_IOS;
}
doc.documentElement.appendChild(messagingIframe);
}
再然後是ios
獲取我們的訊息佇列時不需要通過iframe
,它能直接獲取執行js
函式返回的資料:
function _fetchQueue () {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;// 直接返回,不需要通過iframe
}
其他部分都是一樣的。
總結
本文分析了一下jsBridge
的原始碼,可以發現其實是個很簡單的東西,但是平時可能就沒有去認真瞭解過它,總想做一些”大“的事情,以至於淪為了一個”好高騖遠“的人,希望各位不要像筆者一樣。
另外本文分析的只是筆者公司的jsBridge
實現,可能有不一樣、更好或更新的實現,歡迎留言探討。