js 相关的代码分析
WebViewJavascriptBridge 是 iOS 开发中常用的类库,用于前端和手机端交互。
这里简单分析其源码,温故而知新。
前端调用代码
代码示例见链接
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);
}
setupWebViewJavascriptBridge(function (bridge) {
var uniqueId = 1;
bridge.registerHandler("testJavascriptHandler", function (
data,
responseCallback
) {
log("ObjC called testJavascriptHandler with", data);
var responseData = { "Javascript Says": "Right back atcha!" };
log("JS responding with", responseData);
responseCallback(responseData);
});
bridge.callHandler("testObjcCallback", { foo: "bar" }, function (response) {
log("JS got response", response);
});
});
前端需要引入上述代码,setupWebViewJavascriptBridge(...)
会执行以下操作:
- 会判断
window.WebViewJavascriptBridge
是否存在
- 如果存在则直接作为参数回调给 function
- 如果不存在:
- 将回调 function 存储到
window.WVJBCallbacks
- 在 dom 上添加 iframe,并且约定地址为
https://__bridge_loaded__
- 原生在 webview 的代理方法中监测到该地址的访问时,会拦截该请求,同时注入 js 代码。执行注入代码时会对
WVJBCallbacks
进行遍历。
WebViewJavascriptBridge_JS.m 代码分析
WebViewJavascriptBridge_JS.m
中首先定义了一个宏:
#define __wvjb_js_func__(x) #x
宏中包含了一个#
,这种写法专业点叫Stringize
, 作用是将宏转为字符串常量。例如:
#define toString(size) #size
printf(toString("good"));
NSLog(@toString("good"));
使用时虽然定义了很多行,其实字符串是在一行里。因此 js 代码开头和每个语句结尾都需要;
, 用来防止其他行代码的影响。
此文件的 js 代码主要是对window.WebViewJavascriptBridge
进行初始化,以及一些消息逻辑处理。 下面是删减过的一些代码:
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC,
};
var sendMessageQueue = [];
var messageHandlers = {};
var responseCallbacks = {};
需要注意的是 alert/prompt/confirm 会阻塞消息的执行,因此这里引入了disableJavscriptAlertBoxSafetyTimeout
来控制,但如果启用的话,可能影响安全性。
下面看下查看核心的两个方法:
_doSend
: h5 调用原生或者原生调用 h5,都会触发此方法。 responseCallback
只有在 h5 调用原生时才有值(h5 调用原生才需要回调;h5 接收到原生消息只需要将处理后的消息返回给原生,不需要 callback)。
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;
}
_dispatchMessageFromObjC
: 处理原生发来的请求。
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,
});
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log(
"WebViewJavascriptBridge: WARNING: no handler for message from ObjC:",
message
);
} else {
handler(message.data, responseCallback);
}
}
}
}
原生代码段
原生代码的处理方法总体来说和 h5 的处理逻辑差不多。
消息拦截
处理的核心就是对自定义消息的拦截:
- 当触发
__bridge_loaded__
请求时,注入WebViewJavascriptBridge_JS.m
中的 js 代码
- 当触发
__wvjb_queue_message__
请求时,获取消息数组(WKFlushMessageQueue:
),并且依次执行消息(flushMessageQueue:
)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) {return;}
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
消息执行
原生调用 js 就比较简单了,核心代码见flushMessageQueue:
,此处执行的消息可能有两种类型:一种是原生调用 h5,另一种是 h5 调用原生后的结果的回调。
- (void)flushMessageQueue:(NSString *)messageQueueString {
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage *message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString *responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString *callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage *msg = @{@"responseId": callbackId, @"responseData": responseData};
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}
总结
整体来说 jsbridge 的代码比较清晰优雅的,很多细节值的学习。