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的功能。