我司的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');// 定义事件名为WebViewJavascriptBridgeReadyreadyEvent.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
实现,可能有不一样、更好或更新的实现,欢送留言探讨。