←首页

记得两年前,和 Aevit 在讨论前端如何和 app 端传递数据的时候,有了解过 JSBridge,由于当时的需求很简单,最终只是暴露一个全局方法给客户端调用。现在这种方式已经满足不了需求了。越来越多的前端与客户端的交互,了解如何高效,并且可维护这类通讯很重要,今天看了 WebViewJSBridge 源码,过了一遍代码,以 iOS 和 JS 交互为例,写篇文章记录一下。

Native 调用 JS 代码

iOS 通过 UIWebView 的 stringByEvaluatingJavaScriptFromString 可以执行一段 JS 代码,并拿到返回值,这段代码的执行环境是全局(window),所以 app 端可以直接调用绑定在 window 对象上的参数或者方法。

JS 调用 Native 代码

JS 没办法直接调用 Native 的代码,但是前端页面发出页面请求的时候,webView 可以拦截到。
这里说的页面请求是指修改页面或者 iframe 的 src, 如果你直接更改当前页面 location 的话,Bridge 就不在了,所以通常做法是通过创建一个临时 iframe 来’发请求’

代码

1
2
// oc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

这里可以拿到 url 通过判断 url 是否为目标来识别,为了不混淆正常请求,提前协商好请求的 url。所以就有了 __bridge_loaded__ 这样的标识符。

1
2
3
4
// oc
#define kCustomProtocolScheme @"wvjbscheme"
#define kQueueHasMessage @"__WVJB_QUEUE_MESSAGE__"
#define kBridgeLoaded @"__BRIDGE_LOADED__"
1
2
3
4
5
6
7
8
9
10
11
// js
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

如果存在 window.WebViewJavascriptBridge 直接 return callback, 否则创建 WVJBCallbacks 将请求缓存下来。
代码里面的 iframe 部分只会执行一次,这个请求告诉 Native 端给页面注入 JS 代码。(没错,Native 给 页面注入代码!这一步,不看代码真想不到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// oc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
if ([_base isCorrectProcotocolScheme:url]) {
if ([_base isBridgeLoadedURL:url]) {
// 注入 Javascript
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
// 执行前端页面里面的代码,拿到 MessageQueue,遍历处理所有 message 并清空页面里面的 MessageQueue
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
}
}

如果 URL 是 BridgeLoadedURL,Native 会往 JS 页面里面插入一段早已准备好的 JS 代码
如果 URL 是 QueueMessageURL,Native 则会去拿 JS 页面里面的 MessageQueue,MessageQueue 是所有 Navtive 和 JS 交互的中间状态,所有请求和响应都存在这里面。
处理 message 的过程,Native 和 JS 各有一份,实现基本一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js
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 });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}

responseCallbacks 是发出请求方的回调数组,所以 Native 和 JS 各有一份 responseCallbacks。

如果 message 带有 callbackId,说明这是请求方发出的消息,当前是响应方,响应方拿到这个 message 之后生成一个 callback 在 handler 中调用,在这个 callback 里面又把 callbackId 转成 responseId 返给请求方
如果 message 带有 responseId,说明这是响应方发出的消息,当前是请求方,请求方拿到这个 message 之后就去自己的 responseCallbacks 里面查找 callback 。

值得一提的是 messageQueue 始终只有一份,举个例子。
Native 向 JS 端发出请求,会生成一个带有 callbackId 的 message,并记录当前请求的 callback 到 responseCallback (如果需要的话) , 然后调用 JS 端的 WebViewJavascriptBridge._handleMessageFromObjC 方法把 message 传递过去,JS 端看到这个 message 带有 callbackId 就知道,这是 Native 端发起的请求,在执行完 hanlder 之后,把 callbackId 转成 responseId,生成一个新的 message push 到 messageQueue 里面,然后通过 iframe 去告知 Native 端。Native 端拦截到 iframe 事件之后,通过 JS 代码去获取 messageQueue ,遍历一遍,找到带有 responseId 的 message(也就是一开始那个 callbackId),再通过 responseId 在 responseCallbacks 中找到之前的 callback ,执行回调。到此一次请求结束。

1
2
3
4
5
6
7
8
9
10
// js
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

JS 端向 Native 端发出请求,会生成一个带有 callbackId 的 message,并记录当前请求的 callback 到 responseCallback (如果需要的话),然后将 message push 到 messageQueue 里面,然后通过 iframe 去告知 Native 端,Native 端拦截到 iframe 事件之后,通过 JS 代码去获取 messageQueue ,遍历一遍,找到带有 callbackId 的 message,Native 看到 message 带有 callbackId,知道这是 JS 端的请求,在执行完 handler 之后,把 callbackId 转成 responseId,调用 JS 的 WebViewJavascriptBridge._handleMessageFromObjC 方法,把 message 传过去,JS 端发现这个 message 带有 responseId,通过 responseId 在 responseCallbacks 中找到之前的 callback ,执行回调。到此一次请求结束。

初始流程图

JSBridge 封装完及其好用,尽可能不引入太多全局变量的情况下,两端通过 registerHandler, callHandler 的方式互相调用,并且都支持异步操作。

备注

  • 如果是一个持续维护的项目,应该尽量避免使用直接调用 JS 的方式传递,除非你自己实现一套 bridge 。
  • app 端和 web 端的交互数据格式只能是字符串,双方拿到数据的时候都需要做解析。
  • 两端要提前约定好,传递数据格式。

轮子

如需转载,请注明出处: http://w3ctrain.com/2017/04/12/js-bridge/