WebViewJavascriptBridge源码分析

在APP的开发过程中,都会通过H5来实现部分功能,H5页面是内嵌在原生应用的WebView组件中。在有的场景下,当两端需要相互通信,但是JavaScript的权限受到限制,比如不能修改系统配置等,这个时候需要委托Native去实现某个功能,并在完成后将结果通知JavaScript。所以我们需要在Native和JavaScript之间就搭建一个通信的桥梁,这个桥梁就是我们所说的JavaScript Bridge,简称 JS Bridge。

通常实现Native与JS桥接的方式有两种:

  1. 通过JavaScriptCore框架
  2. 通过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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (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:方法的实现:

1
2
3
4
- (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就要显得难一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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/闭包。在这个过程中,callHandlerregisterHandler等函数都是通过JS代码注入的,这些代码都在Native端,那它是如何成功执行这些函数的呢?关键在于https://__bridge_loaded__这个url。在使用了这个url后,WKWebView的NavigationDelegate会拦截这个请求,并注入JS代码。相关实现如下:

1
2
3
4
5
6
7
8
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// ...
// isBridgeLoadedURL函数中的kBridgeLoaded即为__bridge_loaded__
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
// ...
}

用泳道图来描述初始化bridge的过程
WebViewJavascriptBridge初始化bridge

JS注册函数,Native调用JS

JS注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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函数

1
2
3
4
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];

内部调用了WebViewJavascriptBridgeBasesendData:responseCallback:handlerName:方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (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的回调函数是如何调用的,在后面会讲到。

1
2
3
4
5
6
7
- (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置为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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];
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
- (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初始化的时候注入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在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的过程
WebViewJavascriptBridgeNative调用JS

Native注册函数,JS调用Native

Native注册函数

1
2
3
4
5
6
7
8
9
10
[_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函数

1
2
3
4
5
6
7
8
9
10
11
12
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函数后的回调函数。

1
2
3
4
5
6
7
8
9
10
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过程中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (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过程中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 精简了_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的过程
WebViewJavascriptBridgeJS调用Native

NNWKWebViewJSBridge

NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的实现过程后,基于这个项目,实现一个轻量级Swift版本JSBridge,并且它仅需要支持WKWebView即可。
相对于WebViewJavascriptBridge,我使用了WKUserContentController简化了初始化和消息传递的实现过程,相对来说会更好理解,消息传递性能也要比拦截Requests的方式要高。

项目地址:NNWKWebViewJSBridge
项目截图:WKWebViewJSBridge_demo