浅析H5与app通信桥梁JsBridge的原理
移动端的混合应用是非常常见的,Hybrid APP、React Native APP或者Flutter都很火爆,开发成本相对较低,迭代快,性能也越来越好。
公司产品的app中也是内嵌了很多H5页面,使用JsBridge进行通信。这里简要分析一下它的原理。
拿到ios app中相关的代码之后,我发现公司使用的是 marcuswestin/WebViewJavascriptBridge 这个开源技术,已经有两年没有更新了,不过项目用得还是挺好的,只是发现安卓上面有一个json字符串转义的小问题。这个开源技术是使用URL scheme的方式实现H5与app通信的,没有兼容问题。还有一种通信方式是Api 直接交互,H5调原生和原生调H5都有提供专门的api,就是一些低版本不支持。
我讲的内容可能不是很全面,可以直接参考这篇文章WebViewJavascriptBridge原理解析
主要流程
这里主要讲解H5页面中使用的 jsBridge.js
文件 和 APP中保留的 WebViewJavascriptBridge.js
文件。Native方面的代码不太懂,所以就不多讲了。
核心部分
核心部分有三个:
- H5 页面中的
jsBridge.js
文件,主要是js桥相关业务逻辑的封装 - APP中保留的一份
WebViewJavascriptBridge.js
文件,最重要的部分,定义全局方法供H5和Native是哟ing。负责接收app发给javascript的消息,并且把javascript环境的消息发送给app - APP 接受javascript环境发过来的消息,处理完之后返回处理结果,主要是app的功能代码
这三部分的关系如下图:
H5 页面中的 jsBridge.js
文件
这一部分还是比较简单的,主要是发起 iframe.src='xxx'
通知Native向webview中注入WebViewJavascriptBridge.js
和 使用 WebViewJavascriptBridge.registerHandler
和 WebViewJavascriptBridge.callHandler
注册、执行原生方法。
关键代码:
// window.WebViewJavascriptBridge对象是 客户端 注入到webview的window全局环境中的
if (window[JS_BRIDGE_NAME]) {
_callback(window[JS_BRIDGE_NAME]);
}
else {
// window.WVJBCallbacks 是事件队列
if (window[MESSAGE_QUEUE_NAME]) {
window[MESSAGE_QUEUE_NAME].push(_callback);
} else {
// 在window.WebViewJavascriptBridge没有挂载之前将调用了的js-bridge方法暂时存储到一个队列中
window[MESSAGE_QUEUE_NAME] = [_callback];
/**
* 这个iframe的作用是 加载并执行 保存在app端的WebViewJavascriptBridge_JS.js文件中的代码,执行完之后在全局放一个window.WebViewJavascriptBridge对象
*/
let WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe);
}, 0);
}
}
function _call() {
/**
* cmd:web端与app端定好的实现功能的名字,如:cmd=login,web端调用之后app就会处理登陆逻辑
* params:web端传给app端的参数
* callback:这个方法回来之后web端要执行的逻辑
*/
let [cmd, params, callback] = parseCallArguments(arguments);
// 这个bridge就是window.WebViewJavascriptBridge,这里使用里面的callHandler方法
_run(bridge => {
bridge.callHandler(cmd, ...params, callback);
});
}
function _register(cmd, callback) {
_run(bridge => {
bridge.registerHandler(cmd, callback);
});
}
// 提供两个方法给具体业务调用
module.exports = {
_call,
_register
};
APP中的 WebViewJavascriptBridge.js
文件
这个文件是最核心的部分,是H5与app通信的桥梁。重要的操作时定义供H5和Nativ调用的全局方法,处理Native发送过来的消息 和 发送消息给Native
WebViewJavascriptBridge全局方法:
// 在全局定义window.WebViewJavascriptBridge对象,供H5调用
window.WebViewJavascriptBridge = {
registerHandler: registerHandler, // 注册事件,H5调用
callHandler: callHandler, // 执行事件,H5调用
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, // 设置 与app通信 不需要需要延迟
_fetchQueue: _fetchQueue, // 获取存储事件数据队列json字符串,app调用
_handleMessageFromObjC: _handleMessageFromObjC // app调用JS的入口方法,app调用
};
处理Native发过来的消息:
其实就是Native调用 window.WebViewJavascriptBridge._handleMessageFromObjC
方法,主要的处理逻辑如下:代码中涉及到一些 responseId
和 callbackId
都是处理回调的一些标识,方便找到哪个消息对应调用哪个回调,具体的逻辑可能要仔细看代码才能知道,具体代码都在marcuswestin/WebViewJavascriptBridge中,我这里就不具体解释了。
// app 给 js 通信,处理从app返回的消息
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
// message = { handlerName, data, callbackId }
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 如果app返回的数据有responseId说明app已经处理好了这个事件
// 处理通过callHandler方法的传递的消息回调
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
// 直接执行回调
responseCallback(message.responseData);
// 执行完后删除回调队列中的方法
delete responseCallbacks[message.responseId];
} else {
// 处理通过registerHandler方法的注册事件
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
// 处理注册事件
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
发送消息给Native:
这里发送消息给Native也是使用Url scheme的方式,只是连接变了。发送消息之前都会把H5传过来的消息存到 sendMessageQueue
这个数组中,Native监听到有消息发送过来就会调用 WebViewJavascriptBridge._fetchQueue
这个方法获取 sendMessageQueue
中的所有消息
/**
* js 给 app 发送消息
* @param {Object} message 事件数据,{ handlerName:handlerName, data:data },cmd + data
* @param {Function} responseCallback H5传过来的回调函数
*/
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
// 存储responseCallback回调函数
responseCallbacks[callbackId] = responseCallback;
// 给message多加一个callbackId属性,message事件数据的id 和 回调函数的id是一致的,这方便app处理完之后执行回调函数
message['callbackId'] = callbackId;
}
// 存储 事件数据
sendMessageQueue.push(message);
// 与app通信的方式,会触发decidePolicyForNavigationAction webview跳转代理。
// 简单的讲就是app会监控下面连接的跳转,当检测到这个跳转app就知道是H5给app传递了消息事件(调用了callHandler方法),
// app就会通过全局_fetchQueue和_handleMessageFromObjC方法处理事件。因为app是可以通过window.WebViewJavascriptBridge调用js方法的。
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
Native获取到js发送过来的消息后的处理
Native 中会使用flushMessageQueue这个方法处理消息,它的处理逻辑跟 WebViewJavascriptBridge._handleMessageFromObjC
是一致的。代码在这里WebViewJavascriptBridge/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.m。
我这里主要想说的是,H5和Native两端各自维护同一个功能列表,是一一对应的。举个例子:
H5这边定义了两个js桥方法:login
和 getUserInfo
,调用后的处理方式H5维护。然后Native这边也需要定义对应的两个 login
和 getUserInfo
方法,这两个方法的具体执行逻辑是Native维护。
也就是H5调用了一个js桥后,Native必须要有对应的处理,否则就会调用失败。
对于注册的js桥方法也是一样,这些方法都是Native主动触发的,如果H5没有处理那H5就不会有任何反馈。
所以对于js桥来说,增加或者修改一个桥是两端都要一起完成的事情,需要一起定义传参、测试和联调。
总结
urlScheme方式的js桥还是挺好理解的,使用marcuswestin/WebViewJavascriptBridge的过程中还是会发现一些bug吧,这种源码级别的bug还真的很棘手呀,作者也有两年多没有维护了,我看github上还有很多人反馈bug呢。随着事件的推进肯定会遇到更多的bug,如果完全吃透源码的话,其实我们可以自己完善代码解决bug。