WebViewJavascriptBridge浅析
WebViewJavascriptBridge是一个Objective-C与JavaScript进行消息互通的三方库。通过WebViewJavascriptBridge,我们可以很方便的实现OC和Javascript互调的功能。WebViewJavascriptBridge实现互调的过程也容易理解,就是在OC环境和Javascript环境各自保存一个相互调用的bridge对象,每一个调用之间都有id和callbackid来找到两个环境对应的处理。从Github上下载源码之后,可以看到核心类主要包含如下几个:
- WebViewJavascriptBridge_JS:Javascript环境的Bridge初始化和处理。负责接收OC发给Javascript的消息,并且把Javascript环境的消息发送给OC。
- WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要负责OC环境的消息处理,并且把OC环境的消息发送给Javascript环境。
- WebViewJavascriptBridgeBase:主要实现了OC环境的Bridge初始化和处理。
下面我们来详细看一下具体的实现(这里的介绍以WKWebView环境为例)。
1.初始化
1.1OC环境的初始化
WebViewJavascriptBridge *bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView ]; //初始化调用 //初始化一个OC环境的桥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; } - (id)init { if (self = [super init]) { //用于保存OC环境注册的方法,key是方法名,value是这个方法对应的回调block self.messageHandlers = [NSMutableDictionary dictionary]; //用于保存交互过程中需要发送给javascirpt环境的消息 self.startupMessageQueue = [NSMutableArray array]; //用于保存OC和javascript环境相互调用的回调模块。通过_uniqueId加上时间戳来确定每个调用的回调 self.responseCallbacks = [NSMutableDictionary dictionary]; _uniqueId = 0; } return self; }
【结论】:所有与Javascript之间交互的信息都存储在messageHandlers和responseCallbacks中。这两个属性记录了OC环境与Javascript交互的信息。
1.2OC环境注册方法
注册一个OC方法给J
avascript调用,并且把它的回调实现保存在messageHandlers
中。具体代码如下:
//注册OC方法,以供Javascript调用 [self.bridge registerHandler:@"testClientCallback" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"Javascript传递数据: %@", data); responseCallback(@"OC发给JS的返回值"); }]; - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { _base.messageHandlers[handlerName] = [handler copy]; }
1.3Javascript初始化&注册方法
这个我们先来看一下WebViewJavascriptBridge上的示例ExampleAPP.html:
function setupWebViewJavascriptBridge(callback) { //第一次调用这个方法的时候,为false if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } //第一次调用的时候,为false if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } //把callback对象赋值给对象 window.WVJBCallbacks = [callback]; //加载WebViewJavascriptBridge_JS中的代码 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) } //驱动所有hander的初始化 setupWebViewJavascriptBridge(function(bridge) { //把WEB中要注册的方法注册到bridge里面 bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) { //OC中传过来的数据 log('ObjC called testJavascriptHandler with', data); //JS返回数据 var responseData = { 'Javascript Says':'Right back atcha!' }; responseCallback(responseData); }) })
【说明】:调用setupWebViewJavascriptBridge函数,并且这个函数传入的参数也是一个函数。参数函数中有在Javascript环境中注册的setupWebViewJavascriptBridge的实现过程中,我们可以发现,如果不是第一次初始化,会通过window.WVJBCallbacks两个判断返回。iframe可以理解为webview中的窗口,当我们改变iframe的src属性的时候,相当于我们浏览器实现了链接的跳转。比如从www.google.com。下面这段代码的目的就是实现一个到https://__bridge_loaded__的跳转。从而达到初始化Javascript环境的bridge的作用。
//加载WebViewJavascriptBridge_JS中的代码 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)
我们知道,只要webview有跳转,就会调用webview的代理方法,我们重点看下面这个代理方法。
- (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; //如果是WebViewJavascriptBridge发送或者接收的消息,则特殊处理。否则按照正常流程处理。 if ([_base isWebViewJavascriptBridgeURL:url]) { //1第一次注入JS代码 if ([_base isBridgeLoadedURL:url]) { [_base injectJavascriptFile]; } else if ([_base isQueueMessageURL:url]) {//处理WEB发过来的消息 [self WKFlushMessageQueue]; } else { [_base logUnkownMessage:url]; } decisionHandler(WKNavigationActionPolicyCancel); return; } //webview的正常代理执行流程 if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) { [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; } else { decisionHandler(WKNavigationActionPolicyAllow); } }
上面的方法中,首先通过isWebViewJavascriptBridgeURL 来判断是普通的跳转还是webViewJavascriptBridge跳转。如果是__bridge_loaded__ 表示是初始化Javascript环境的消息;如果是__WVJB_QUEUE_MESSAGE__ 表示是发送Javascript消息。我们继续看几个核心方法的实现:
#define kOldProtocolScheme @"wvjbscheme" #define kNewProtocolScheme @"https" #define kQueueHasMessage @"__wvjb_queue_message__" #define kBridgeLoaded @"__bridge_loaded__" //是否是WebViewJavascriptBridge框架相关的链接 - (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url { if (![self isSchemeMatch:url]) { return NO; } return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url]; } //是否是WebViewJavascriptBridge发送或者接收的消息 - (BOOL)isSchemeMatch:(NSURL*)url { NSString* scheme = url.scheme.lowercaseString; return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme]; } //是WebViewJavascriptBridge发送的消息还是WebViewJavascriptBridge的初始化消息 - (BOOL)isQueueMessageURL:(NSURL*)url { NSString* host = url.host.lowercaseString; return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage]; } //是否是https://__bridge_loaded__这种初始化加载消息 - (BOOL)isBridgeLoadedURL:(NSURL*)url { NSString* host = url.host.lowercaseString; return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded]; }
接下来调用injectJavascriptFile方法,将WebViewJavascriptBridge_JS中的方法注入到webview中并且执行,从而达到初始化Javascript环境的brige的作用。
- (void)injectJavascriptFile { NSString *js = WebViewJavascriptBridge_js(); //把javascript代码注入webview中执行 [self _evaluateJavascript:js]; //javascript环境初始化完成以后,如果有startupMessageQueue消息,则立即发送消息 if (self.startupMessageQueue) { NSArray* queue = self.startupMessageQueue; self.startupMessageQueue = nil; for (id queuedMessage in queue) { [self _dispatchMessage:queuedMessage]; } } } //把javascript代码写入webview - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand { [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]; return NULL; }
那么WebViewJavascriptBridge_JS到底是怎么实现的呢?
NSString * WebViewJavascriptBridge_js() { #define __wvjb_js_func__(x) #x // BEGIN preprocessorJSCode static NSString * preprocessorJSCode = @__wvjb_js_func__( ;(function() { //如果已经初始化了,则返回 if (window.WebViewJavascriptBridge) { return; } if (!window.onerror) { window.onerror = function(msg, url, line) { console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line); } } //初始化Bridge对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法 window.WebViewJavascriptBridge = { registerHandler: registerHandler, //JS中注册方法 callHandler: callHandler, //JS中调用OC中的方法 disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, //把消息转换成JSON串 _handleMessageFromObjC: _handleMessageFromObjC //OC调用JS的入口方法 }; var messagingIframe; //用于存储消息列表 var sendMessageQueue = []; //用于存储消息 var messageHandlers = {}; //通过下面两个协议组合来确定是否是特定的消息,然后拦击 var CUSTOM_PROTOCOL_SCHEME = 'https'; var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; //oc调用js的回调 var responseCallbacks = {}; //消息对应的id var uniqueId = 1; //是否设置消息超时 var dispatchMessagesWithTimeoutSafety = true; //web端注册一个消息方法 function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } //web端调用一个OC注册的消息 function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') { responseCallback = data; data = null; } _doSend({ handlerName:handlerName, data:data }, responseCallback); } function disableJavscriptAlertBoxSafetyTimeout() { dispatchMessagesWithTimeoutSafety = false; } //把消息从JS发送到OC,执行具体的发送操作 function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); //存储消息的回调ID responseCallbacks[callbackId] = responseCallback; //把消息对应的回调ID和消息一起发送,以供消息返回以后使用 message['callbackId'] = callbackId; } //把消息放入消息列表 sendMessageQueue.push(message); //下面这句话会出发JS对OC的调用,让webview执行跳转操作,从而可以在 //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler //中拦截到JS发给OC的消息 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; } //把消息转换成JSON字符串返回 function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; return messageQueueString; } //处理从OC返回的消息 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 }); }; } //获取JS注册的函数 var handler = messageHandlers[message.handlerName]; if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); } else { //调用JS中的对应函数处理 handler(message.data, responseCallback); } } } } //OC调用JS的入口方法 function _handleMessageFromObjC(messageJSON) { _dispatchMessageFromObjC(messageJSON); } messagingIframe = document.createElement('iframe'); messagingIframe.style.display = 'none'; messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; document.documentElement.appendChild(messagingIframe); //注册_disableJavascriptAlertBoxSafetyTimeout方法,让OC可以关闭回调超时,默认是开启的 registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout); //执行_callWVJBCallbacks方法 setTimeout(_callWVJBCallbacks, 0); //初始化WEB中注册的方法。这个方法会把WEB中的hander注册到bridge中 //下面的代码其实就是执行WEB中的callback函数 function _callWVJBCallbacks() { var callbacks = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); } } })(); ); // END preprocessorJSCode #undef __wvjb_js_func__ return preprocessorJSCode; };
从上面可以看到,整个类就是一个立即执行的Javascript方法。
- 首先会初始化一个WebViewJavascriptBridge对象,并且这个对象是赋值给window对象,这里的window对象可以理解为webview。所以如果在OC环境中要调用js方法,就可以通过在加上具体方法来调用。
- WebViewJavascriptBridge对象中有Javascript环境注入的提供给OC调用的方法registerHandler,javascript调用OC环境方法的callHandler。
- _fetchQueue这个方法的作用就是把Javascript环境的方法序列化成JSON字符串,然后传入OC环境再转换。
- _handleMessageFromObjC就是处理OC发给Javascript环境的方法。
在这个文件中也初始化了一个iframe实现webview的url跳转功能,从而激发webview代理方法的调用。
messagingIframe = document.createElement('iframe'); messagingIframe.style.display = 'none'; messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; document.documentElement.appendChild(messagingIframe);
上面的src就是https://__wvjb_queue_message__/。这个是javascript发送的OC的第一条消息,目的和上面OC环境的startupMessageQueue一样,就是在javascript环境初始化完成以后,把javascript要发送给OC的消息立即发送出去。
1.4OC发消息给Javascript
- (void)p_callJSHandler:(id)sender { id data = @{ @"OC调用JS方法": @"OC调用JS方法的参数" }; [self.bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) { NSLog(@"JS 响应数据: %@", response); }]; } /** OC调用JS方法 @param handlerName JS中提供的方法名称 @param data 参数 @param responseCallback 回调block */ - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback { [_base sendData:data responseCallback:responseCallback handlerName:handlerName]; } - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { //所有信息存入的字典 NSMutableDictionary* message = [NSMutableDictionary dictionary]; if (data) { message[@"data"] = data; } if (responseCallback) { NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId]; self.responseCallbacks[callbackId] = [responseCallback copy]; message[@"callbackId"] = callbackId; } if (handlerName) { message[@"handlerName"] = handlerName; } [self _queueMessage:message]; } //把OC消息序列化、并且转化为JS环境的格式,然后在主线程中调用_evaluateJavascript - (void)_dispatchMessage:(WVJBMessage*)message { NSString *messageJSON = [self _serializeMessage:message pretty:NO]; [self _log:@"SEND" json:messageJSON]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"]; NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON]; if ([[NSThread currentThread] isMainThread]) { [self _evaluateJavascript:javascriptCommand]; } else { dispatch_sync(dispatch_get_main_queue(), ^{ [self _evaluateJavascript:javascriptCommand]; }); } }
打印javascriptCommand,结果如下:
WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC调用JS方法\":\"OC调用JS方法的参数\"},\"handlerName\":\"testJavascriptHandler\"}');
实际上就是执行JS环境中的_handleMessageFromObjC方法:
//处理从OC返回的消息 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 }); }; } //获取JS注册的函数 var handler = messageHandlers[message.handlerName]; if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); } else { //调用JS中的对应函数处理 handler(message.data, responseCallback); } } } } //把消息从JS发送到OC,执行具体的发送操作 function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); //存储消息的回调ID responseCallbacks[callbackId] = responseCallback; //把消息对应的回调ID和消息一起发送,以供消息返回以后使用 message['callbackId'] = callbackId; } //把消息放入消息列表 sendMessageQueue.push(message); //下面这句话会出发JS对OC的调用,让webview执行跳转操作,从而可以在 //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler //中拦截到JS发给OC的消息 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
最重要的是最后面的通过改变iframe的messagingIframe.src
,只有这样才能触发webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
从而在OC中处理javascript环境触发过来的回调。
//第一次注入JS代码 if ([_base isBridgeLoadedURL:url]) { [_base injectJavascriptFile]; } else if ([_base isQueueMessageURL:url]) {//处理WEB发过来的消息 [self WKFlushMessageQueue]; } else { [_base logUnkownMessage:url]; } decisionHandler(WKNavigationActionPolicyCancel);
这里会走[self WKFlushMessageQueue];
方法:
//把消息或者WEB回调发送到OC - (void)WKFlushMessageQueue { [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) { if (error != nil) { NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error); } [_base flushMessageQueue:result]; }]; }
最后执行flushMessageQueue,通过Javascript传过来的responseId获取对应的
WVJBResponseCallback
,执行这个block。到这里从OC发送消息到Javascript并且Javascript返回消息给OC的流程就完成了。
1.5JS发消息给OC
bridge.callHandler('testObjcHandler', {'foo': 'bar'}, function(response) { log('JS got response', response)--> }) //web端调用一个OC注册的消息 function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') { responseCallback = data; data = null; } _doSend({ handlerName: handlerName, data: data }, responseCallback); } //JS调用OC方法,执行具体的发送操作 function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); //存储消息的回调ID responseCallbacks[callbackId] = responseCallback; //把消息对应的回调ID和消息一起发送,以供消息返回以后使用 message['callbackId'] = callbackId; } //把消息放入消息列表 sendMessageQueue.push(message); //下面这句话会发起JS对OC的调用,让webview执行跳转操作,从而可以在 //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler //中拦截到JS发给OC的消息 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
具体执行和OC调用javascript过程一样。
2.总结
- 分别在OC环境和Javascript环境都保存一个bridge对象,里面维持着requestId、callbackId,以及每个id对应的具体实现。
- OC通过Javascript环境的
window.WebViewJavascriptBridge
对象来找到具体的方法,然后执行。 - Javascript通过改变iframe的src来唤起webview的代理方法
webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
从而实现把Javascript消息发送给OC的功能。