共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。
我司的 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
实现,可能有不一样、更好或更新的实现,欢送留言探讨。