共计 6819 个字符,预计需要花费 18 分钟才能阅读完成。
缘由:网上其实有很多讲解 WebViewJavascriptBridge 原理的文章,但都着重了 Native 端,今天从一个纯前端角度出发,抓住核心脉络讲解下原理,清晰明了,一文即懂。
通信的基础:
native 端到 js 端。native 能获取到 window 环境,执行 JS。
js 端到 native 端。native 能截获 H5 页面跳转,故而 JS 端可以通过动态创建 iframe 来告诉 native,我发请求了。(其实就是在 window 下维护了一个 messageQueue 数组,然后 js 创建个 iframe,告诉 native,我向你发请求了,但具体是什么请求,url 里面是不会体现的,需要 native 去遍历 messageQueue 数组, 毕竟 native 能取到 window 环境)。
首先初始化 JS 端环境。
初始化方法 setupWebViewJavascriptBridge,里面的字段都是约定的,因为在 native 执行初始化 JS 的时候,会用到,比如 WVJBCallbacks。因为通信的过程是异步的(动态创建 iframe, 捕获跳转)。所以 setupWebViewJavascriptBridge 是异步的,并且以后调用 native 提供的方法也会是异步的。并且最好是在 setupWebViewJavascriptBridge 的回调函数中调用,可以保证初始化成功了。而回调函数里面会把 WebViewJavascriptBridge 当做参数返回。
上图是网上偷懒截的图,到时候底部放个链接。
重点:window.WebViewJavascriptBridge,所有的交互都是通过这个 WebViewJavascriptBridge 对象来完成的。初始化的过程就是动态创建一个 iframe,将 iframe 的 src 设置为 https://__bridge_loaded__,然后插入到页面中。前面通信基础说过,natvie 能够截获 h5 跳转,当 Native 捕获到当前 URL,并且其值等于 https://__bridge_loaded__(当前 URL 是约定成俗的),就会注入一段自执行的代码(假设其为 bridge,下面统一称呼了), 挂载 WebViewJavascriptBridge 到 window 上。等下会着重说下这段自执行的代码是如何工作的。
if (window.WebViewJavascriptBridge),if (window.WVJBCallbacks),保证了初始化只执行一次。WVJBCallbacks 这个字段用于还没有初始化的时候,保存回调函数。当我们通知 native 端进行初始化,并且初始化之后,bridge 里面会去遍历 WVJBCallbacks 中的回调函数,并将 WebViewJavascriptBridge 当做参数注入。执行完后会 delete window.WVJBCallbacks。
下图是我们真正调用一个 native 的方法,假设去获取用户信息,WebViewJavascriptBridge 是重点,下面会详细讲解下这个对象。
export function getUserData() {
return new Promise((resolve, reject) => {
setupWebViewJavascriptBridge((WebViewJavascriptBridge) => {
WebViewJavascriptBridge.callHandler(‘xxxx’, params, (data) => {
resolve(data);
});
})
})
}
到了这里,其实我们前端需要做的已经完了。我再理一理顺序。getUserData,去调用 native 提供的方法。先调用 setupWebViewJavascriptBridge,里面有做是否初始化的判断。然后回调函数里面,我们就能取到 WebViewJavascriptBridge 对象,对象上面挂载 callHandler 了方法,’xxxx’ 代表着和 native 端约定好的方法,执行即可。
WebViewJavascriptBridge
最上面的时候说过,native 可以执行 JS,能获取到 window。所以接下来重点讲一下连接 native 和 js 的(bridge)桥梁是如何运作的。什么时候初始化 bridge?重复说一下。setupWebViewJavascriptBridge 第一次调用的时候,会创建一个 iframe,src 指向 https://__bridge_loaded__。当 native 截获到这个请求的时候,判断为 bridge_loaded,就会注入一段自执行的代码进行 WebViewJavascriptBridge 初始化。接下来看代码:
// 如果已经初始化了,则返回。
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log(“WebViewJavascriptBridge: ERROR:” + msg + “@” + url + “:” + line);
}
}
// 初始化一些属性。
var messagingIframe;
// 用于存储消息列表
var sendMessageQueue = [];
// 用于存储消息
var messageHandlers = {};
// 通过下面两个协议组合来确定是否是特定的消息,然后拦击。
var CUSTOM_PROTOCOL_SCHEME = ‘https’;
var QUEUE_HAS_MESSAGE = ‘__wvjb_queue_message__’;
//oc 调用 js 的回调
var responseCallbacks = {};
// 消息对应的 id
var uniqueId = 1;
// 是否设置消息超时
var dispatchMessagesWithTimeoutSafety = true;
//web 端注册一个消息方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//web 端调用一个 OC 注册的消息
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == ‘function’) {
responseCallback = data;
data = null;
}
_doSend({handlerName: handlerName, data: data}, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
// 把消息转换成 JSON 字符串返回
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//OC 调用 JS 的入口方法
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
// 初始化桥接对象,OC 可以通过 WebViewJavascriptBridge 来调用 JS 里面的各种方法。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
// 处理从 OC 返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 回调
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {// 主动调用
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData});
};
}
// 获取 JS 注册的函数
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log(“WebViewJavascriptBridge: WARNING: no handler for message from ObjC:”, message);
} else {
// 调用 JS 中的对应函数处理
handler(message.data, responseCallback);
}
}
}
}
// 把消息从 JS 发送到 OC,执行具体的发送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = ‘cb_’ + (uniqueId++) + ‘_’ + new Date().getTime();
// 存储消息的回调 ID
responseCallbacks[callbackId] = responseCallback;
// 把消息对应的回调 ID 和消息一起发送,以供消息返回以后使用。
message[‘callbackId’] = callbackId;
}
// 把消息放入消息列表
sendMessageQueue.push(message);
// 下面这句话会出发 JS 对 OC 的调用
// 让 webview 执行跳转操作,从而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到 JS 发给 OC 的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ‘://’ + QUEUE_HAS_MESSAGE;
}
messagingIframe = document.createElement(‘iframe’);
messagingIframe.style.display = ‘none’;
//messagingIframe.body.style.backgroundColor=”#0000ff”;
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ‘://’ + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
// 注册_disableJavascriptAlertBoxSafetyTimeout 方法,让 OC 可以关闭回调超时,默认是开启的。
registerHandler(“_disableJavascriptAlertBoxSafetyTimeout”, disableJavscriptAlertBoxSafetyTimeout);
// 执行_callWVJBCallbacks 方法
setTimeout(_callWVJBCallbacks, 0);
// 初始化 WEB 中注册的方法。这个方法会把 WEB 中的 hander 注册到 bridge 中。
// 下面的代码其实就是执行 WEB 中的 callback 函数。
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
这里着重讲 JS 如何调 native 方法的。window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
这个对象就是上文中回调函数获取到的 WebViewJavascriptBridge 对象。callHandler 表示执行某个约定的方法。上面的源码注释很清晰了,源码就不过多解读了。接下来我们举一个完整的列子来阐述整个过程。
重点 getUserData => 调用 setupWebViewJavascriptBridge => 因为是第一次调用进行初始化, 会将回调函数保存到 WVJBCallbacks 中。=> 动态创建 ifram,native 截获,注入代码进行 brige 初始化 => WebViewJavascriptBridge 初始化 => 第一次执行所以会触发_callWVJBCallbacks,遍历上面的 WVJBCallbacks 数组,并且将 WebViewJavascriptBridge 作为参数传入,执行回调。=> 回调中执行的就是 getUserData 里面的 WebViewJavascriptBridge.callHandler(‘xxxx’)=》callHandler 执行的其实就是__doSend 方法。=》_doSend 里面会将 getUserData 里面的回调函数保存在全局对象变量 responseCallbacks 中,key 则是自增的 ID。并且把 getUserData 所调用的方法名,参数,key,都放在一个对象中 (message),并将这个 message, 存到另外一个全局数组变量 sendMessageQueue 中。=> 动态创建一个 ifram,src 为__wvjb_queue_message__,也就是说创建的 ifram, 一般就两种地址,一个是告诉 native 进行初始化,一个是告诉 native 可以轮询消息队列 sendMessageQueue 了。=> native 拦截到 URL,遍历全局变量 sendMessageQueue,执行 getUserData 所需要的方法,这里就是 XXXX,并组装参数 => 调用另一个_handleMessageFromObjC 方法,解析参数,得到一开始的自增 ID,从全局 responseCallbacks 中取到真正的回调函数执行。=> over 了。完整的交互就是这个样子的了。
上面的流程已经很清楚了,如果有不懂,或者有错误的地方欢迎指正。这是 js 到 native 端的消息过程,还有 native 调 js 的,其实过程差不多。参考下文把 WebViewJavascriptBridge 原理解析