一篇文章學會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的運行機制的文章就介紹到這瞭,更多相關jsBridge 運行機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Javascript之JSBridge初探
- 詳解如何讓頁面與 iframe 進行通信
- 與iframe進行跨域交互的解決方案(推薦)
- vue內嵌iframe跨域通信的實例代碼
- vue組件和iframe頁面的相互傳參問題及解決