在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
1 | + (instancetype)bridgeForWebView:(WKWebView*)webView { |
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/闭包。在这个过程中,callHandler
、registerHandler
等函数都是通过JS代码注入的,这些代码都在Native端,那它是如何成功执行这些函数的呢?关键在于https://__bridge_loaded__
这个url。在使用了这个url后,WKWebView的NavigationDelegate会拦截这个请求,并注入JS代码。相关实现如下:
1 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { |
用泳道图来描述初始化bridge的过程
JS注册函数,Native调用JS
JS注册函数
1 | // JS注册函数给Native调用 |
Native调用JS函数
1 | id data = @{ @"greetingFromObjC": @"Hi there, JS!" }; |
内部调用了WebViewJavascriptBridgeBase
的sendData: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 | - (void)injectJavascriptFile { |
1 | - (void)_dispatchMessage:(WVJBMessage*)message { |
_dispatchMessage
将之前拼装好的message传给JS,用JS bridge的 _handleMessageFromObjC
函数处理Native的调用请求,_handleMessageFromObjC
函数是在JS bridge初始化的时候注入的。
1 | function _handleMessageFromObjC(messageJSON) { |
当WKWebView的代理方法拦截到https://__wvjb_queue_message__
这个url的时候,就会调用WKFlushMessageQueue
方法
1 | if ([_base isQueueMessageURL:url]) { |
执行_fetchQueue()
这个JS函数,并在completionHandler
这个block内返回JS中sendMessageQueue
的信息,从而获取带responseId
的message。接着执行Native的flushMessageQueue
方法。
1 | // 在flushMessageQueue方法中完成了Native调用JS函数后的回调 |
用泳道图来描述Native调用JS的过程
Native注册函数,JS调用Native
Native注册函数
1 | [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { |
JS调用Native函数
1 | bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) { |
在Native调用JS的过程也使用了_doSend
函数,它的作用是为了能调用Native调用JS函数之后的回调函数。在JS调用Native的过程中,_doSend
函数是为了调用OC函数(与函数名对应的block),responseCallback则是代表JS调用OC函数后的回调函数。
1 | function _doSend(message, responseCallback) { |
之后的逻辑在OC调用JS中已经描述过了,直到执行flushMessageQueue:
方法前都是一样的
这里就不再重复。接着看一下flushMessageQueue:
方法在JS调用Native过程中的实现:
1 | - (void)flushMessageQueue:(NSString *)messageQueueString{ |
关于_queueMessage:
方法前面已经分析过,这里就不再重复,接着看一下_dispatchMessageFromObjC
函数在JS调用Native过程中的实现:
1 | // 精简了_doDispatchMessageFromObjC,只保留调用JS回调的部分 |
接着用泳道图来描述下JS调用Native的过程
NNWKWebViewJSBridge
NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的实现过程后,基于这个项目,实现一个轻量级Swift版本JSBridge,并且它仅需要支持WKWebView即可。
相对于WebViewJavascriptBridge,我使用了WKUserContentController
简化了初始化和消息传递的实现过程,相对来说会更好理解,消息传递性能也要比拦截Requests的方式要高。
项目地址:NNWKWebViewJSBridge
项目截图: