UIWebView 已被全面废弃,故本文只分析 WKWebView 实现。源码见 WebViewJavascriptBridge
先来看下在 WKWebView
下,是怎么使用JSBridge 的。
WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
webView.navigationDelegate = self;
[self.view addSubview:webView];
[WebViewJavascriptBridge enableLogging];
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
所以,看来 JSBridge 对象是 WebViewJavascriptBridge
实现的。
WebViewJavascriptBridge.h宏定义如下
#if defined __MAC_OS_X_VERSION_MAX_ALLOWED
#define WVJB_PLATFORM_OSX
#define WVJB_WEBVIEW_TYPE WebView
#define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WebViewJavascriptBridgeBaseDelegate>
#define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<WebViewJavascriptBridgeBaseDelegate, WebPolicyDelegate>
#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED
#import <UIKit/UIWebView.h>
#define WVJB_PLATFORM_IOS
#define WVJB_WEBVIEW_TYPE UIWebView
#define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<UIWebViewDelegate>
#define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
#endif
所以,在 iOS 上,其本质是
NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
那 WKWebView
也是这个类,也遵守 UIWebViewDelegate
? 是否有点奇怪?
看其内部实现,就会发现,当系统支持 WebKit
时,并且用户传的是 WKWebView
时,实例化 WebViewJavascriptBridge
对象时,就直接以 WKWebView
进行了初始化。
+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}
#endif
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
[NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
return nil;
}
所以,接下来,我们就去看 WKWebViewJavascriptBridge
实例化对象
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
上述代码又实例化了一个很重要成员变量 WebViewJavascriptBridgeBase
整个 JSBridege objc 代码的实现就集中在了 WKWebViewJavascriptBridge
和 WebViewJavascriptBridgeBase
,此外还有一个 JS 代码的注入文件 WebViewJavascriptBridge_js
,整个库的核心可以说就是这几个文件了。
WebViewJavascriptBridge 加载
可以看到,
WebViewJavascriptBridge
加载过程主要如下:
- h5 网页加载,内部添加了一个不展示的 iframe,其 src 属性设置为了
https://__bridge_loaded__
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
- iframe src 加载时,其请求被
WKWebView
WKNavigationDelegate
代理方法拦截
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
- 内部判断为加载 WebViewJavascriptBridge 请求,执行注入 js 代码
WebViewJavascriptBridge_js
,实现 JSBridge,挂载在 window 上。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
注入 js 同时,又添加了一个 iframe
元素,其 src 设置为了 https://__wvjb_queue_message__
,注入的时候就会加载,然后被 WKNavigationDelegate
拦截。
如果 load web 之前,就调用了 callHandler,则会先存储起来,先执行注入 WebViewJavascriptBridge_js
,然后再去 sendData。
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
native 调用 h5
以 native 调用H5 testJavascriptHandler
方法为例, native 调用 h5 主要经历如下几个步骤:
- h5 首先注册
testJavascriptHandler
- native 发送的数据,被包装成 json 字符串作为参数,然后使用
WebViewJavascriptBridge._handleMessageFromObjC('%@');
在 WKWebView 执行 js 代码。json 字符串包含:data
callbackId
handlerName
参数。同时将回调根据callbackId
存储起来。(callbackId
使用objc_cb_
+ 自增 id) - js 方法
_dispatchMessageFromObjC
来处理数据。如果有callbackId
,则会使用_doSend
方法,将callbackId
转为responseId
,加入testJavascriptHandler
返回的responseData
, 然后将数据存储到sendMessageQueue
对象中,然后加载messagingIframe
src
,src 链接为https://__wvjb_queue_message__
-
WKNavigationDelegate
代理拦截到 messagingIframe 重新加载 信息,执行 js 代码WebViewJavascriptBridge._fetchQueue();
获取存在sendMessageQueue
中数据。 - 从
sendMessageQueue
中获取到responseId
,根据responseId
取到存储在2 中存储在responseCallbacks
的 callback handler,然后 native 调用 handler,传入 js 的responseData
。
h5 调用 native
以 h5 调用 native
testObjcCallback
handler 为例:
- native 需要先注册
testObjcCallback
handler - h5 调用 handler
testObjcCallback
bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
log('JS got response', response)
})
js
_doSend()
方法,处理调用,将handlerName
data
callbackId
包装成JSON 字典对象, 存到sendMessageQueue
中,并存储回调(如有调用有回调),然后设置messagingIframe
,让其重新加载https://__wvjb_queue_message__
。其中callbackId
生成规则'cb_'+(uniqueId++)+'_'+new Date().getTime()
WKNavigationDelegate 代理拦截
https://__wvjb_queue_message__
,处理调用。首先通过WebViewJavascriptBridge._fetchQueue();
获取 js 数据,即sendMessageQueue
中数据。根据
sendMessageQueue
中数据,是否有callbackId
。如果有callbackId
, 即在 native handler 中,传入responseData
,并将callbackId
转为responseId
, 然后再次WKWebView
执行 js 代码。h5
WebViewJavascriptBridge
中 通过_handleMessageFromObjC
方法处理调用。拿到responseData
和responseId
, 根据responseId
,找到3中存储的回调,然后执行。