JsBridge是Hybrid项目中一种实现H5与Native两者之间通讯的成熟、安全的解决方案。
基础原理
JsBridge,顾名思义,它是一座桥,架在H5和Native之间的桥;通过这座桥,H5这边可以调用原生的方法,比如调用Native的分享、相机等等这些H5无法很好满足的功能;同时,Native也可以通过这座桥,更为方便、规范地使用约定好的方法。
那JsBridge的通讯机理是什么呢?其实就是url scheme的触发与拦截。
H5调用Native
JsBridge中定义好一套url scheme协议,H5端根据协议触发响应的url,页面载体webview拦截住url后,解析发现是约定好的通讯协议,Native根据解析结果调用响应的原生方法,进行响应的操作。
Native调用H5
那么,Native是怎么调用H5中的方法的呢?不好意思,是Native创建了H5的载体webview,所以Native是爸爸,它可以直接调用页面中的全局函数方法。但是既然使用了JsBridge,那么肯定是要按照协议执行的,“执法办事”是必要的,不然代码没有规范,难以落成文档,后期迭代维护成本会递增。
在使用JsBridge的情况下,JsBridge将会挂载在页面的window变量中,H5将Native所需调用的方法注册到JsBridge中;Native然后根据约定的方式对这些方法进行传参和调用,具体实现会在下面源码解析板块进行解读。
源码剖析
JsBridge的源码不多,不到200行,下面我们按照“自己手写一个JsBridge”的思路来对源码进行搜索拆分。
H5调用Native
根据事先机理,我首先想到的是,把实现H5调用Native的代码捋出来。那么,我们需要在页面中来触发url scheme。我们会想到window.location.href、a标签或者iframe等等。源码中使用的是iframe.src来触发,为什么呢?因为window.location.href和a标签其实是一样,其如果短时间内触发多次,webview只会捕获最后一次请求而忽略之前的
,这个解释援引搜索内容,我没有去进行验证,有兴趣的同学可以验证下哦。下面先看第一段代码,描述我就直接写在在代码的注释中了。
var messagingIframe; // 触发url scheme的iframe
var bizMessagingIframe; // 又定义了一个,这个我们不管,再往下看看
var sendMessageQueue = []; // 存放H5向Native发送的消息队列
var receiveMessageQueue = []; // 存放Native发送给H5的消息
var messageHandlers = {}; // 可供native调用的方法,通过registerHandler注册存入messageHandlers
var CUSTOM_PROTOCOL_SCHEME = 'yy'; // url scheme中用以标志协议的字段
var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; // url scheme中另一个字段
var responseCallbacks = {}; // 存放callback的对象
var uniqueId = 1; // 用于生成callbackId的标志之一,每次生成后都会加1
有了进行通讯的url,我们下面就来看看是怎么进行触发的吧。挂载window上的WebViewJavascriptBridge中定义一些方法,callHandler就是用来给H5调用Native使用的。
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
init: init, // 初始化
send: send, // 单纯的H5发送消息
registerHandler: registerHandler, // 用以注册Native需要调用的js方法
callHandler: callHandler, // 发送消息,并指明需要调用的Native方法
_fetchQueue: _fetchQueue, // Native调用,用以获取sendMessageQueue中的消息
_handleMessageFromNative: _handleMessageFromNative // Native调用,给H5发消息
};
callHandler只是对_doSend函数的简单封装,看来具体的实现是在_doSend中
function callHandler(handlerName, data, responseCallback) {
_doSend({ // 发送的message包含调用的native方法名称以及传输的数据
handlerName: handlerName,
data: data
}, responseCallback);
}
/**
* [_doSend H5调用native,对callHandler的丰富]
* @param {[type]} message [调用消息体]
* @param {[type]} responseCallback [回调函数]
* @constructor
* @return {[type]} [description]
*/
function _doSend(message, responseCallback) {
// 如果传入回调函数,则为回调函数生成一个callbackId,
// 以callbackId为key,将回调函数存入responseCallbacks对象中,用于之后回调使用
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); // callbackId
responseCallbacks[callbackId] = responseCallback; // 将callbackId维护字在responseCallbacks中
message.callbackId = callbackId; // message新加一个callbackId字段
// 当前message包含内容
// {
// callbackId: callbackId, // 回调函数id
// handlerName: handlerName, // 调用的Native方法名
// data: data // 传参数据
// }
}
sendMessageQueue.push(message);
// 通过iframe通知客户端有消息了
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
这边我们便可以来解释下最初为啥要创建两个iframe。messagingIframe并不是用来传输消息的,而只是告诉Native有消息需要处理了,真正用来传输消息的iframe是bizMessagingIframe。
// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
Native得到通知后,通过_fetchQueue方法将sendMessageQueue中消息取出并通过bizMessagingIframe触发url scheme来传输消息。说实话,这边的实现有点迷糊,感觉没必要多中间通知这个环节,可能作者另有考虑吧。
Native调用H5
WebViewJavascriptBridge中定义的_handleMessageFromNative就是用来处理Native对H5的调用
// Native调用H5主要分为两种
// 1、H5调用Native时,传入的callback,Native执行完后,现在来执行回调
// 2、Native调用H5方法,可以选择传入需要回调的Native函数
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() { // 异步挂起处理,不会影响同步任务
var message = JSON.parse(messageJSON);
var responseCallback;
if (message.responseId) { // 如果消息中有responseId,代表这是native执行完,这个responseId其实就是_doSend函数中传入的callbackId
responseCallback = responseCallbacks[message.responseId]; // 根据responseId取出之前存入responseCallbacks对象中的回调函数
if (!responseCallback) {
return;
}
responseCallback(message.responseData); // 传入message.responseData参数,执行callback
delete responseCallbacks[message.responseId]; // callback执行完后,从responseCallbacks队列中删除该callback
} else { // native调用H5的方法
// Native调用H5方法过程与之前类似,也可以要求有个回调
if (message.callbackId) { // callbackId代表,native要求H5方法执行完后,给native一个回调
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId, // responseId告诉native这是你要的回调
responseData: responseData
});
};
}
var handler = WebViewJavascriptBridge._messageHandler; // init时定义的H5默认方法,如果native没有指明handlerName的情况下,就会调用默认方法
if (message.handlerName) { // native指定了要调用H5方法
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
至此,JsBridge中H5与Native之间通讯的主体实现代码就已经讲完了。还是蛮简单的,其实就是定义了两者通讯的一种协议。
最后
年后本想着写一篇文章来阐述Hybrid实现原理的完整技术解读,奈何在原生部分有短板,就只能写个JsBridge来个源码解析来解解馋了~