WebViewJavaScriptBridge是IOS中JS和OC交互的常用框架,支持以下兩種調用:
1. OC端的方法如下
Method Frome OC
Method 1 是注冊一個OC的方法--testObjcCallback,handler是JS掉用的內容,responseCallback是將OC處理返回給JS的回調(對應的是上述第2種調用);
Method 2 是調用JS的方法的testJavascriptHandler方法,@{ @"foo":@"before ready" }是需要傳遞的參數,responseCallback是將JS處理結果返回給OC的回調(對應的是上述的第1種調用)
2. JS端的方法如下
Method Frome JS
Method 1 是JS注冊一個方法供OC調用,responseCallback(responseData)是將處理結果返回OC。
Method 2 是在點擊了一個按鈕之后JS調用OC的方法,{'foo': 'bar'}是給OC的參數,response是OC處理后返回給JS的數據。
注:JS中是可以不寫;號的,這和swift一樣
JS調用OC,OC將處理結果回調給JS:要想被JS調用,我們首先要注冊一個handler,和回調的? ? ? ? ? ? block,注冊時候以鍵值對的形式存儲這個block,handler,當JS調用OC時調用webView:shouldStartLoadWithRequest:navigationType:這個方法,根據JS傳來的數據,找到之前保存的Block并且調用,同時新建一個需要把處理結果回調給JS的Blcok,OC處理完結果之后調用剛才創建的Block利用stringByEvaluatingJavaScriptFromString將處理結果返回給JS。
OC調用JS時與此類似。基于這個流程,我們來看WebViewJavaScriptBridge的實現過程。
原理
接下來我們來分析從頁面加載到OC和JS互相調用的整個過程:
一、準備工作
當加載HTML文件的時候調用[webView loadHTMLString:appHtml baseURL:baseURL];,這時會調用:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {if(webView != _webView) {returnYES; }NSURL*url = [request URL];? ? __strongWVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;if([_base isWebViewJavascriptBridgeURL:url]) {if([_base isBridgeLoadedURL:url]) {? ? ? ? ? ? [_base injectJavascriptFile];? ? ? ? }elseif([_base isQueueMessageURL:url]) {NSString*messageQueueString = [self_evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];? ? ? ? ? ? [_base flushMessageQueue:messageQueueString];? ? ? ? }else{? ? ? ? ? ? [_base logUnkownMessage:url];? ? ? ? }returnNO;? ? }elseif(strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {return[strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];? ? }else{returnYES;? ? }}
在這個方法中判斷URL的類型,如果是WebViewJavascriptBridgeURL那么就會判斷是BridgeLoadedURL,QueueMessageURL還是未知的URL,在首次調用時是返回YES的,然后的URL就是BridgeLoadedURL,我們在看它的判斷條件[self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];Scheme是自己設置的https,那么BridgeLoaded(__bridge_loaded__)是什么呢?我們看ExampleApp.html文件,發現它的script標簽中有這么一段代碼:
functionsetupWebViewJavascriptBridge(callback){if(window.WebViewJavascriptBridge) {returncallback(WebViewJavascriptBridge); }if(window.WVJBCallbacks) {returnwindow.WVJBCallbacks.push(callback); }window.WVJBCallbacks = [callback];varWVJBIframe =document.createElement('iframe');? ? WVJBIframe.style.display ='none';? ? WVJBIframe.src ='https://__bridge_loaded__';document.documentElement.appendChild(WVJBIframe);? ? setTimeout(function(){document.documentElement.removeChild(WVJBIframe) },0)}setupWebViewJavascriptBridge(function(bridge){varuniqueId =1functionlog(message, data){varlog =document.getElementById('log')varel =document.createElement('div')? ? ? ? el.className ='logLine'el.innerHTML = uniqueId++ +'. '+ message +':
'+JSON.stringify(data)if(log.children.length) { log.insertBefore(el, log.children[0]) }else{ log.appendChild(el) }? ? }
在這里我們發現了https://__bridge_loaded__這個iframe的src,并且在接下來調用setupWebViewJavascriptBridge時這個src會當做一個請求,這時會調用shouldStartLoadWithRequest這個方法。此時就滿足了isBridgeLoadedURL這個請求。這時就會調用
[_base injectJavascriptFile]
注入一個JS文件,這個JS文件的主要內容是(篇幅問題,有刪減):
window.WebViewJavascriptBridge = {registerHandler: registerHandler,callHandler: callHandler,disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,_fetchQueue: _fetchQueue,_handleMessageFromObjC: _handleMessageFromObjC};varmessagingIframe;varsendMessageQueue = [];varmessageHandlers = {};varCUSTOM_PROTOCOL_SCHEME ='https';varQUEUE_HAS_MESSAGE ='__wvjb_queue_message__';varresponseCallbacks = {};varuniqueId =1;vardispatchMessagesWithTimeoutSafety =true;functionregisterHandler(handlerName, handler){? ? messageHandlers[handlerName] = handler;}functioncallHandler(handlerName, data, responseCallback){? ? _doSend();}functiondisableJavscriptAlertBoxSafetyTimeout(){? ? dispatchMessagesWithTimeoutSafety =false;}function_doSend(message, responseCallback){? ? sendMessageQueue.push(message);? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;}function_fetchQueue(){varmessageQueueString =JSON.stringify(sendMessageQueue);? ? sendMessageQueue = [];returnmessageQueueString;}function_dispatchMessageFromObjC(messageJSON){if(dispatchMessagesWithTimeoutSafety) {? ? ? ? setTimeout(_doDispatchMessageFromObjC);? ? }else{? ? ? ? _doDispatchMessageFromObjC();? ? }function_doDispatchMessageFromObjC(){? ? ? ? }? ? }}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);registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);setTimeout(_callWVJBCallbacks,0);function_callWVJBCallbacks(){varcallbacks =window.WVJBCallbacks;deletewindow.WVJBCallbacks;for(vari=0; i
下面我們來分析下注入的JavaScript的內容。
給window對象添加一個屬性WebViewJavascriptBridge(JS中可以直接給對象添加屬性),這個對象包含以下內容:
? 1) registerHandler:注冊調用方法
? 2)callHandler:調用OC時的方法
? 3)disableJavscriptAlertBoxSafetyTimeout:超時時彈框是否展示的標示
? 4)_fetchQueue:獲取Queue對象的方法
? 5)_handleMessageFromObjC:處理OC調用的方法
2.定義了一系列的變量來存儲數據
messagingIframe:iframe標簽,當我們的WebView加載它的時候,會調用其中的src,src就是調用請求的URL。
1)sendMessageQueue:message數組? 2)messageHandlers:handler對象 *JS中{}表示對象*? 3)CUSTOM_PROTOCOL_SCHEME:scheme標示? 4)QUEUE_HAS_MESSAGE:有Message標識? 5)responseCallbacks:回調對象? 6)uniqueId:唯一標示ID
進過系列一的剖析,我們明白了使用WebViewJavaScriptBridge前需要做的準備工作,那么接下來,我們一起探討OC和JS相互調用的具體執行過程以及其中的要點。
二、 JS調用OC,然后OC將處理結果返回JS
1. OC首先注冊JS將調用的方法
OC調用registerHandler:,這時將其調用信息存儲在messageHandlers字典中以handlerName為Key,給JS處理結果的Block為Value(_base.messageHandlers[handlerName] = [handler copy]);
2. 在JS中調用被注冊的方法
JS調用
bridge.callHandler('testObjcCallback', {'foo':'bar'},function(response){? ? ? ? ? ? log('JS got response', response)? ? ? ? })
來調用上文OC注冊的方法,這個brige就是上文注入JS代碼時候創建的,我們再它內部做了什么。
functioncallHandler(handlerName, data, responseCallback){if(arguments.length ==2&&typeofdata =='function') {? ? ? ? responseCallback = data;? ? ? ? data =null;? ? }? ? _doSend({handlerName:handlerName,data:data }, responseCallback);}
這里判斷了參數類型,如果傳入的參數只有兩個,并且第二個是function類型,那么就將第二個參數變為callBack,data置空,將handlerName和data轉化成一個對象的兩個屬性并傳給_doSend()。
function_doSend(message, responseCallback){if(responseCallback) {varcallbackId ='cb_'+(uniqueId++)+'_'+newDate().getTime();? ? ? ? ? ? ? ? responseCallbacks[callbackId] = responseCallback;? ? ? ? ? ? ? ? message['callbackId'] = callbackId;? ? ? ? ? ? }? ? ? ? ? ? sendMessageQueue.push(message);? ? ? ? ? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;? ? ? ? }
這里的responseCallback是JS先調用OC然后OC調用JS時才會有的,如果這種情況,那么需要用唯一的標識(callbackId),來將這個responseCallback存儲在responseCallbacks中,并且給message添加callbackId這個屬性。這個數值會在下次OC調用JS的時候作為唯一的Key被用到。軟后將message放入:sendMessageQueue隊列中,然后拼接src。
3. 在回掉方法中攔截相應的方法,然后調用block.
經過方法步驟2,會調用下面的回調方法
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType{}
在這個方法中調用
NSString*messageQueueString = [self_evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];? [_base flushMessageQueue:messageQueueString];
首先獲取JS中的messageQueue(步驟2中的sendMessageQueue),然后調用flushMessageQueue:方法:
idmessages = [self_deserializeMessageJSON:messageQueueString];for(WVJBMessage* messageinmessages) {if(![message isKindOfClass:[WVJBMessageclass]]) {NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messageclass], message);continue;? ? }? ? [self_log:@"RCVD"json:message];/////////*********OC先調用了JS,JS再調用了OC*********///////////NSString* responseId = message[@"responseId"];if(responseId) {//調用之前存儲的BolckWVJBResponseCallback responseCallback = _responseCallbacks[responseId];? ? ? ? responseCallback(message[@"responseData"]);? ? ? ? [self.responseCallbacks removeObjectForKey:responseId];/////////*********JS先調用OC,OC再調用JS*********////////////// 這里是JS先調用OC的時候存儲的是 JS的回調函數}else{// JS先調用的OC,OC再調用JSWVJBResponseCallback responseCallback =NULL;NSString* callbackId = message[@"callbackId"];if(callbackId) {? ? ? ? ? ? responseCallback = ^(idresponseData) {if(responseData ==nil) {? ? ? ? ? ? ? ? ? ? responseData = [NSNullnull];? ? ? ? ? ? ? ? }//JS調用OC時候的存儲(后續OC調用JS返回計算結果)WVJBMessage* msg = @{@"responseId":callbackId,@"responseData":responseData };? ? ? ? ? ? ? ? [self_queueMessage:msg];? ? ? ? ? ? };? ? ? ? }else{? ? ? ? ? ? responseCallback = ^(idignoreResponseData) {// Do nothing};? ? ? ? }? ? ? ? ? ? ? ? WVJBHandler handler =self.messageHandlers[message[@"handlerName"]];if(!handler) {NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);continue;? ? ? ? }//調用OC的Block,同時,如果OC調用responseCallback,則調用_queueMessage進行相應的處理handler(message[@"data"], responseCallback);? ? }}
這里先將返回的JSON字符串轉換成對象,這里的字符串是調用
function_fetchQueue(){varmessageQueueString =JSON.stringify(sendMessageQueue);? ? sendMessageQueue = [];returnmessageQueueString;? }
獲取的,這里將sendMessageQueue轉為JSON,然后將其置空,這里為啥使用數組而不用對象來存儲呢?因為可能JS還沒有處理結束就有兩次調用,要保證他們不丟失使用了數組。然后判斷數組中的Message對象是否有responseId(JS調用OC第一次時存儲的),這里沒有responseId所以走else:如果有callbackId(在JS中作為回調用的),定義responseCallback,這個block就是OC將處理結果返回給JS時用到的block。如果沒有callbackId說明,不需要回調JS,這個時候responseCallback為空。最后調用步驟1中存儲在messageHandlers對象中的block,并且將剛才創建的responseCallback作為參數傳入,以便OC將計算結果傳遞給JS。
4. OC將計算結果返回給JS
[_bridge registerHandler:@"testObjcCallback"handler:^(iddata, WVJBResponseCallback responseCallback) {NSLog(@"testObjcCallback called: %@", data);? ? responseCallback(@"response form oc's call back");? }];
在handler的最后一步調用responseCallback()將處理結果回調給JS。這個responseCallback()就是我們在步驟3中創建的responseCallback。我們再來看這個block。看步驟3可以看到這個其內部調用
[self_queueMessage:msg];? [self_dispatchMessage:message];
在_dispatchMessage內部執行:
NSString* javascriptCommand = [NSStringstringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
接下來JS中的_handleMessageFromObjC就會接收到OC傳過來處理結果。
function_doDispatchMessageFromObjC(){varmessage =JSON.parse(messageJSON);varmessageHandler;varresponseCallback;if(message.responseId) {? ? ? ? ? ? responseCallback = responseCallbacks[message.responseId];if(!responseCallback) {return;? ? ? ? ? ? }? ? ? ? ? ? responseCallback(message.responseData);deleteresponseCallbacks[message.responseId];? ? ? ? }else{// OC先調用JS是用到}? ? }
這個時候我們看到了步驟三中的responseId的作用了,這時候responseId就表明了是OC將處理結果傳遞給JS并不需要JS再調用OC了,這時只調用responseCallback(message.responseData);將數據傳給JS。
這樣我們就完成了JS調用OC,然后OC將結果回調給JS的全部過程。
三、OC調用JS,然后JS將處理結果返回給OC
1. JS注冊相應的方法供回調
同OC注冊方法時候一樣,JS也是用一個messageHandlers對象來存儲
functionregisterHandler(handlerName, handler){? ? messageHandlers[handlerName] = handler;? ? }
2. OC調用JS時存儲調用信息
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {NSMutableDictionary* message = [NSMutableDictionarydictionary];if(data) {? ? ? ? ? ? message[@"data"] = data;? ? ? ? }if(responseCallback) {NSString* callbackId = [NSStringstringWithFormat:@"objc_cb_%ld", ++_uniqueId];self.responseCallbacks[callbackId] = [responseCallbackcopy];? ? ? ? ? ? message[@"callbackId"] = callbackId;? ? ? ? }if(handlerName) {? ? ? ? ? ? message[@"handlerName"] = handlerName;? ? ? ? }? ? ? ? [self_queueMessage:message];? ? }
這里使用message字典來存儲參數,方法名,使用responseCallbacks來存儲JS處理完之后,需要回調的Block(這里為了確保多次調用不會覆蓋之前的調用,使用了唯一的callbackId)。
同上文所述,最終會調用
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {return[_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];? ? }
3. JS調用_dispatchMessageFromObjC
這時message沒有responseId,會走else,
if(message.callbackId) {varcallbackResponseId = message.callbackId;? ? ? ? ? ? ? ? ? ? responseCallback =function(responseData){? ? ? ? ? ? ? ? ? ? ? ? _doSend({handlerName:message.handlerName,responseId:callbackResponseId,responseData:responseData });? ? ? ? ? ? ? ? ? ? };? ? ? ? ? ? ? ? }varhandler = messageHandlers[message.handlerName];if(!handler) {console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);? ? ? ? ? ? ? ? }else{? ? ? ? ? ? ? ? ? ? handler(message.data, responseCallback);? ? ? ? ? ? ? ? } 這里定義了需要給OC傳遞結果的`responseCallback`,取出之前注冊的`handler`:`messageHandlers[message.handlerName]`,然后調用這個`handler`,并將這個`responseCallback`作為參數傳進去,`handler(message.data, responseCallback);`
4. JS將結果回傳給OC
在步驟三中調用handler:function(data, responseCallback){? ? ? ? ? ? log('ObjC called testJavascriptHandler with', data)varresponseData = {'Javascript Says':'Right back atcha!'}? ? ? ? ? ? log('JS responding with', responseData)? ? ? ? ? ? responseCallback(responseData)? ? ? ? } 在這個`handler`的結尾調用步驟三種的`responseCallback`(傳入的只有數據沒有回調),根據步驟三可以看出來其會調用`_doSend`方法。該方法中由于沒有傳進去回調,所以不會給message對象添加`callbackId`,只調用? ? ? ? sendMessageQueue.push(message);? ? ? ? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;
這是由于含有responseId(在步驟三中的_doSend調用時設置),所以只會取出之前存儲的block,并且將結果回傳給OC:
//調用之前存儲的BolckWVJBResponseCallback responseCallback = _responseCallbacks[responseId];? ? ? ? responseCallback(message[@"responseData"]);? ? ? ? [self.responseCallbacks removeObjectForKey:responseId];
至此,OC和JS交互的所有邏輯已介紹完畢(WKWebView實現方式相同),總結下兩種情景的回調,其實現方式及其相似,正如文章開頭的總結。