博客链接 WebViewJavascriptBridge源码分析
在APP的开发过程中,都会通过H5来实现部分功能,H5页面是内嵌在原生应用的WebView组件中。在有的场景下,当两端需要相互通信,但是JavaScript的权限受到限制,比如不能修改系统配置等,这个时候需要委托Native去实现某个功能,并在完成后将结果通知JavaScript。所以我们需要在Native和JavaScript之间就搭建一个通信的桥梁,这个桥梁就是我们所说的JavaScript Bridge,简称 JS Bridge。
通常实现Native与JS桥接的方式有两种:
- 通过JavaScriptCore框架
- 通过Webview拦截请求的方式(WebViewJavascriptBridge使用的方式)
marcuswestin/WebViewJavascriptBridge是使用第2种方式实现在用于在WKWebView和UIWebView中,JS与Native相互发送消息。
与其他OC的三方库不同,WebViewJavascriptBridge的实现包括OC和JS两部分,因此只看OC部分的代码我们是无法理解这个bridge是如何实现两端通信的。
WebViewJavascriptBridge中的类的作用
-
WebViewJavascriptBridgeBase
:OC端桥接基础服务类,维护OC端开放给JS端的方法以及OC回调方法,实现OC向JS发送数据的具体逻辑。 -
WebViewJavascriptBridge_JS
:维护了一份JS代码,用于JS环境的注入。同时维护JS端的bridge对象,管理JS端注册的方法集合以及回调方法集合,面向Web端提供注册JS方法、调用OC端方法的接口。 -
WKWebViewJavascriptBridge
:基于WKWebView的OC端交互逻辑处理类,面向OC业务层,提供了注册OC方法、调用JS方法等接口。 -
WebViewJavascriptBridge
:基于UIWebView的的OC端交互逻辑处理类,与WKWebViewJavascriptBridge
的功能一致。
WebViewJavascriptBridge源码解析
WebViewJavascriptBridge
的实现可以说是双向的过程,无论是JS端还是Native端都包含以下三部分内容:
- bridge初始化
- 本端注册函数共另一端调用
- 调用另一端函数
目前App已经取消对UIWebView的支持,所以我们只需要看WKWebView相关部分的实现即可。
bridge初始化
bridge初始化分为Native初始化bridge和JS初始化bridge。在使用WebView的时候,都是从Native端打开页面开始,因此先分析Native初始化bridge。
Native初始化bridge
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
- (void)_setupInstance:(WKWebView*)webView {
_webView = webView;
// 将webView的navigationDelegate设为WKWebViewJavascriptBridge对象自身
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
// WKWebViewJavascriptBridge对象需要实现_evaluateJavascript:代理方法
_base.delegate = self;
}
- (void)reset {
[_base reset];
}
WKWebViewJavascriptBridge
中_evaluateJavascript:
方法的实现:
- (NSString*)_evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
关于JS代码的注入,可以使用WKUserContentController
,也可以使用evaluateJavaScript:completionHandler:
这个函数,WebViewJavascriptBridge
使用后者实现JS注入,因此需要将webView的navigationDelegate
设为WKWebViewJavascriptBridge
对象自身,并在代理方法中调用evaluateJavaScript:completionHandler:
函数。
在Native初始化后,就要使用load
之类的方法加载页面与JS代码,这进入到JS初始化bridge过程。
JS初始化bridge
相对于Native初始化bridge来说,JS初始化bridge就要显得难一些。
<script>
function setupWebViewJavascriptBridge(callback) {
// window表示浏览器窗口
// WebViewJavascriptBridge就是bridge对象。
// 如果有bridge对象则直接调用callback并传入bridge对象。
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 如果有WVJBCallbacks则将回调函数push到数组里,后面初始化bridge时会统一遍历调用callback,并传入bridge。
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是Native端注入的一个函数
setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 执行调用setupWebViewJavascriptBridge函数,bridge就是对象。
// 在WebViewJavascriptBridge_JS.m文件中对bridge的定义
// window.WebViewJavascriptBridge = {
// registerHandler: registerHandler,
// callHandler: callHandler,
// disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
// _fetchQueue: _fetchQueue,
// _handleMessageFromObjC: _handleMessageFromObjC
// };
setupWebViewJavascriptBridge(function (bridge) {
// ...
// JS注册函数供Native调用
bridge.registerHandler('testJavascriptHandler', function (data, responseCallback) {
// ...
})
// ...
})
</script>
在JS初始化bridge过程中,会直接调用setupWebViewJavascriptBridge(callback)
函数,callback相当于block/闭包。在这个过程中,callHandler
、registerHandler
等函数都是通过JS代码注入的,这些代码都在Native端,那它是如何成功执行这些函数的呢?关键在于https://__bridge_loaded__
这个url。在使用了这个url后,WKWebView的NavigationDelegate会拦截这个请求,并注入JS代码。相关实现如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// ...
// isBridgeLoadedURL函数中的kBridgeLoaded即为__bridge_loaded__
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
// ...
}
用泳道图来描述初始化bridge的过程
JS注册函数,Native调用JS
JS注册函数
// JS注册函数给Native调用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' }
responseCallback(responseData)
})
// WebViewJavascriptBridge_JS.m中的JS代码
// 保存JS函数与函数名的映射关系
var messageHandlers = {};
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Native调用JS函数
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
内部调用了WebViewJavascriptBridgeBase
的sendData:responseCallback:handlerName:
方法。
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
// JS函数所需的参数
if (data) message[@"data"] = data;
// responseCallback:Native调用JS函数后的回调函数
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
// 保存Native回调
self.responseCallbacks[callbackId] = [responseCallback copy];
// 保存回调方法的id
message[@"callbackId"] = callbackId;
}
// JS函数名
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
在Native调用JS的函数时,有时Native需要JS调用Native的回调函数返回一些数据,因此需要保存回调函数的一些信息,关于Native的回调函数是如何调用的,在后面会讲到。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
在开发过程中,有可能Native调用JS函数的时候,JS端还没有完成bridge准备工作。bridge是在decidePolicyForNavigationAction:
的代理方法中执行injectJavascriptFile
方法才完成的,但是
callHandler
可能在viewWillAppear
的时候调用,此时没有完成JS端bridge的初始化,所以先存入startupMessageQueue
中,等准备完成后, 再统一调用 startupMessageQueue
中的Message到JS,并将startupMessageQueue
置为空。
- (void)injectJavascriptFile {
// 注入JS bridge的环境代码
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
// 对于一些提前调用的callHandler,在注入JS初始化代码后,会统一发送,并清空startupMessageQueue
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
- (void)_dispatchMessage:(WVJBMessage*)message {
// 序列化message
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
// 省略对messageJSON的处理
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
_dispatchMessage
将之前拼装好的message传给JS,用JS bridge的 _handleMessageFromObjC
函数处理Native的调用请求,_handleMessageFromObjC
函数是在JS bridge初始化的时候注入的。
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
function _dispatchMessageFromObjC(messageJSON) {
// 忽略dispatchMessagesWithTimeoutSafety部分
_doDispatchMessageFromObjC();
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 是否有responseId,对于Native调用JS函数所传过来的message来说是没有该字段的
// if (message.responseId) {
// // ...
// }
// 处理Native调用JS函数的message
if (message.callbackId) {
// 是否含有Native回调
var callbackResponseId = message.callbackId;
responseCallback = function (responseData) {
// _doSend函数只传了message,另外没有responseCallback参数
_doSend({
handlerName: message.handlerName,
// 如果Native传过来的message有回调,那么JS端需要传入一个responseId,这样Native端才能通过responseId这个key
// 在responseCallbacks字典中找到对应的Native回调
responseId: callbackResponseId,
responseData: responseData
});
};
}
// 通过handlerName获取到对应的JS函数,并调用。
// messageHandlers保存了JS bridge的函数名和回调函数
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
// 这个_doSend是精简之后的实现,_doDispatchMessageFromObjC中的_doSend函数没有传递responseCallback参数
function _doSend(message) {
// sendMessageQueue保存message信息,这个message信息是给Native回调时候用的
sendMessageQueue.push(message);
// src = https://__wvjb_queue_message__,WKWebView的代理方法优惠拦截这个url,从而调用WKWebViewJavascriptBridge的KFlushMessageQueue方法
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
当WKWebView的代理方法拦截到https://__wvjb_queue_message__
这个url的时候,就会调用WKFlushMessageQueue
方法
if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
}
- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
NSLog(@"%@", result);
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
执行_fetchQueue()
这个JS函数,并在completionHandler
这个block内返回JS中sendMessageQueue
的信息,从而获取带responseId
的message。接着执行Native的flushMessageQueue
方法。
// 在flushMessageQueue方法中完成了Native调用JS函数后的回调
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 省略messageQueueString的有效性判断
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 省略对Message类型的校验
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}
}
}
用泳道图来描述Native调用JS的过程
Native注册函数,JS调用Native
Native注册函数
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
//
responseCallback(@"Response from testObjcCallback");
}];
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
// messageHandlers用来保存OC函数与函数名的映射关系
_base.messageHandlers[handlerName] = [handler copy];
}
JS调用Native函数
bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
log('JS got response', response)
})
// JS端callHandler的实现
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
在Native调用JS的过程也使用了_doSend
函数,它的作用是为了能调用Native调用JS函数之后的回调函数。在JS调用Native的过程中,_doSend
函数是为了调用OC函数(与函数名对应的block),responseCallback则是代表JS调用OC函数后的回调函数。
function _doSend(message, responseCallback) {
// 如果有JS回调,则使用responseCallbacks保存JS回调
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;
}
之后的逻辑在OC调用JS中已经描述过了,直到执行flushMessageQueue:
方法前都是一样的
这里就不再重复。接着看一下flushMessageQueue:
方法在JS调用Native过程中的实现:
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 省略messageQueueString的有效性判断
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 省略对Message类型的校验
// 忽略关于responseId的实现
// 对于JS调用Native所传过来的message来说是没有responseId字段的
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
// 判断是否有JS回调
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
// _queueMessage: -> _dispatchMessage: -> JS: _handleMessageFromObjC -> JS: _dispatchMessageFromObjC
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
// 通过handlerName获取到对应的Native函数,并调用。
// messageHandlers保存了Native bridge的函数名和回调函数
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
// 执行Native函数
handler(message[@"data"], responseCallback);
}
}
关于_queueMessage:
方法前面已经分析过,这里就不再重复,接着看一下_dispatchMessageFromObjC
函数在JS调用Native过程中的实现:
// 精简了_doDispatchMessageFromObjC,只保留调用JS回调的部分
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var responseCallback;
// 如果有JS回调,那么OC传过来的message必然存在responseId字段
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
// 调用JS回调
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}
}
接着用泳道图来描述下JS调用Native的过程
NNWKWebViewJSBridge
NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的实现过程后,基于这个项目,实现一个轻量级Swift版本JSBridge,并且它仅需要支持WKWebView即可。
相对于WebViewJavascriptBridge,我使用了WKUserContentController
简化了初始化和消息传递的实现过程,相对来说会更好理解,消息传递性能也要比拦截Requests的方式要高。
项目地址:NNWKWebViewJSBridge
项目截图: