JSBridge 设计和实现
Hybrid 中的桥梁
Hybrid 应用混合使用了原生和 Web 技术,原生应用使用移动平台的 WebView 控件,加载 Web 资源,并通过向其开放一些接口(如加速器、摄像头等),增强了 Web 应用的能力。如此 Hybrid 应用既有原生应用的能力,又有了 Web 的热更新、端代码统一、开发迭代快的优势。Hybrid 中实现不同层沟通交互的桥梁即是 JSBridge,它连通了原生层语言( Java、Swift等)与网页层 JavaScript 语言,使得彼此可以相互调用。
JSBridge 的实现技术
JS 调用原生层
Android 中 JS 与 Native 的通信,主要有两种:
-
注入对象 通过注入对象到 window 对象上,连接原生应用层与 JS 层,实现相互交互, Android 使用 JavaScriptInterface addJavaScriptInterface(Object object, String name) 方法负责把 object 对象暴露成 JavaScript 中的 name 对象
-
拦截 scheme 覆盖 WebChromeClient 中 onJsAlerton、JsConfirm、onJsPrompt 方法来对 JS 的 alert、confirm、prompt 进行捕获,或者覆盖 WebviewClient 中的 shouldOverrideUrlLoading 方法来拦截url 跳转
WebviewClient 和 WebChromeClient
两者通过 Webview 的 setWebViewClient 方法作为参数传入,拦截 Webview 的相关行为,当 Webview 发生相应的行为后,即可在 WebviewClient 和 WebChromeClient 对象中对应的回调方法中获得,覆盖两者中的方法,即可监听特定事件做相应的操作。
- WebviewClient 影响网页 view 的行为,如页面开始加载和结束的onPageStarted、onPageFinished,url 跳转的 shouldOverrideUrlLoading。
- WebChromeClient 浏览器的行为,如 onJsAlert、onJsConfirm、onJsPrompt和信息打印onConsoleMessage
iOS UIWebView 有个特性:在 UIWebView 内发起的所有网络请求,都可以通过 delegate 函数在 Native 层得到通知。这样,我们可以在 webview 中捕获 url scheme 的触发(原理是利 shouldStartLoadWithRequest)
WKWebView 通过 WKUserContentController 的 addScriptMessageHandler 方法注入一个JS对象名,当 JS 调用 window.webkit.messageHandlers.senderModel.postMessage({body: ‘sender message’}) 时可在didReceiveScriptMessage 方法中收到相应的 message 对象
原生层调用 JS
-
Android 通过 WebView 的 loadUrl 或 evaluateJavascript方法
-
iOS 通过UIWebView 的 stringByEvaluatingJavaScriptFromString 或 WKWebview 的 evaluateJavaScript 方法
JSBridge 的设计
异步通信
通过 JSBridge,JS 可使用原生层的资源、网络访问能力、接口处理能力等,这些大部分情况下都是异步的(尽管部分情况 JSBridge 原生层与 JS 的通信技术可同步取得结果),同时也为做到 Android 与 iOS 两端统一,接口最小化,双方的所有的通信都设计为异步的。
双方对等
JSBridge的通信是全双工通信,原生层与JS层都即可作为请求调用方,也可作为响应回复方。JS 可调用原生层的接口函数,同时原生层也可调用 JS 的接口函数,只要一方注册过处理函数,对方调用时即可找到对应的处理函数。
处理函数注册与调用
被调用方通过 registerHandler 将接口开发出来,注册 handler 后调用名与对应的处理函数,形成映射。
var messageHandlers = {};
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handle
}
调用 callHandler 采用传入函数名、参数、回调函数的方式异步使用。当响应方数据返回时,为能够正确的将响应数据传给对应的回调函数,需为回调函数生成对应的请求 id,放入映射表内。这样响应方将响应数据与请求 id 回传后,通过请求 id,可以将响应数据正确的传递给回调函数。
var uniqueId = 1;
var responseCallbacks = {};
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
var message = {handlerName: handlerName, data: data};
// 如果有回调,生成回调 id
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
// _doSend 与原生层约定实现,schema 拦截或对象注入等
_doSend(message);
}
通信链路的实现
- 传输数据
schema 拦截可实现两端统一的代码,对象注入需根据 navigator.userAgent 做平台区分
function _doSend(message) { var messageString = JSON.stringify(message); iframe.src = 'JSBridge://send' + encodeURIComponent(messageString); }
- 接收数据
_handleMessageFromNative 接收原生层传来的数据,主要任务为:
- 接收原生层的请求响应数据;
- 响应原生层的函数调用请求
也就是说此处,此通道需要做请求数据与响应数据的区分,如果是原生层的响应数据,将响应数据根据响应 id 分发给映射表中的回调函数;如果是请求数据,则根据请求函数名,调用对应注册的handler 做处理。为保证线程安全,JS 层的实现将其用setTimeout 包裹,变为一个 macro task。
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
// 与原生层约定,请求时传入的 id 值作为响应 id 返回,表示此数据为响应数据
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) {
// 将请求 id 键名变为响应 id 键名,id 值不变,表示一次交互完成响应
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = _messageHandler; // 默认 handler
if (message.handlerName) {
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);
}
}
}
});
}
查询与订阅
JS 需要获取原生层的信息时,大部分通过发起一次查询请求的方式获取,但有些情形 JS 需要对某些维护在原生层的信息进行订阅,如长连接的推送消息、事件监听等。此时通过查询的方式不再适用,需建立对原生层信息的订阅关系,在 registerHandler 与 callHandler 的基础上封装一步实现,即 JS 先 registerHandler 一个订阅信息处理函数,开发给原生层,并通过 callHandler 告知原生层,这样当原生层的信息更新后,直接调用该订阅处理函数,完成信息的推送。
function subscribe(onMessageName, handler) {
if(typeof onMessageName !== 'string' && typeof handler !== 'function') {
throw new Error('invalid subscribe parameters');
}
JSBridge.registerHandler(onMessageName, handler);
JSBridge.callHandler('subscribe', {onMessageName: onMessageName}, function(data) {
if(data.status !== 'OK') {
throw new Error(data.error);
}
});
return function() {
JSBridge.callHandler('unSubscribe', {onMessageName: onMessageName});
delete messageHandlers[onMessageName];
}
}
end
三方类库