本篇爲你們介紹一個優秀的開源小項目:WebViewJavascriptBridge。javascript
它優雅地實現了在使用UIWebView時JS與ios 的ObjC nativecode之間的互調,支持消息發送、接收、消息處理器的註冊與調用以及設置消息處理的回調。前端
就像項目的名稱同樣,它是鏈接UIWebView和Javascript的bridge。在加入這個項目以後,他們之間的交互處理方式變得很友好。java
在native code中跟UIWebView中的js交互的時候,像下面這樣:ios
//發送一條消息給UI端並定義回調處理邏輯 [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) { if (error) { NSLog(@"Uh oh - I got an error: %@", error); } NSLog(@"objc got response! %@ %@", error, responseData); }];
而在UIWebView中的js跟native code交互的時候也變得很簡潔,好比在調用處理器的時候,就能夠定義回調處理邏輯:git
//調用名爲testObjcCallback的native端處理器,並傳遞參數,同時設置回調處理邏輯 bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { <span style="white-space:pre"> </span>log('Got response from testObjcCallback', response) })
一塊兒來看看它的實現吧,它總共就包含了三個文件:github
WebViewJavascriptBridge.h WebViewJavascriptBridge.m WebViewJavascriptBridge.js.txt
它們是以以下的模式進行交互的:web
很明顯:WebViewJavascriptBridge.js.txt主要用於銜接UIWebView中的web page,而WebViewJavascriptBridge.h/m則主要用於與ObjC的native code打交道。他們做爲一個總體,其實起到了一個「橋樑」的做用,這三個文件封裝了他們具體的交互處理方式,只開放出一些對外的涉及到業務處理的API,所以你在須要UIWebView與Native code交互的時候,引入該庫,則無需考慮太多的交互上的問題。整個的Bridge對你來講都是透明的,你感受編程的時候,就像是web編程的前端和後端同樣清晰。編程
簡單地羅列一下它能夠實現哪些功能吧:後端
出於表達上的須要,對於UIWebView相關的我就稱之爲UI端,而objc那端的處理代碼稱之爲Native端。app
【1】UI端
(1) UI端在初始化時支持設置消息的默認處理器(這裏的消息指的是從Native端接收到的消息)
(2) 從UI端向Native端發送消息,並支持對於Native端響應後的回調處理的定義
(3) UI端調用Native定義的處理器,並支持Native端響應後的回調處理定義
(4) UI端註冊處理器(供Native端調用),並支持給Native端響應處理邏輯的定義
【2】 Native端
(1) Native端在初始化時支持設置消息的默認處理器(這裏的消息指的是從UI端發送過來的消息)
(2) 從Native端向UI端發送消息,並支持對於UI端響應後的回調處理邏輯的定義
(3) Native端調用UI端定義的處理器,並支持UI端給出響應後在Native端的回調處理邏輯的定義
(4) Native端註冊處理器(供UI端調用),並支持給UI端響應處理邏輯的定義
UI端以及Native端徹底是對等的兩端,實現也是對等的。一段是消息的發送端,另外一段就是接收端。這裏爲引發混淆,須要解釋一下我這裏使用的「響應」、「回調」在這個上下文中的定義:
(1) 響應:接收端給予發送端的應答
(2) 回調:發送端收到接收端的應答以後在接收端調用的處理邏輯
下面來分析一下源碼:
WebViewJavascriptBridge.js.txt:
主要完成了以下工做:
(1) 建立了一個用於發送消息的iFrame(經過建立一個隱藏的ifrmae,並設置它的URL 來發出一個請求,從而觸發UIWebView的shouldStartLoadWithRequest回調協議)
(2) 建立了一個核心對象WebViewJavascriptBridge,並給它定義了幾個方法,這些方法大部分是公開的API方法
(3) 建立了一個事件:WebViewJavascriptBridgeReady,並dispatch(觸發)了它。
代碼解讀
UI端實現
對於(1),相應的代碼以下:
/* *建立一個iFrame,設置隱藏並加入到DOM中 */ function _createQueueReadyIframe(doc) { messagingIframe = doc.createElement('iframe') messagingIframe.style.display = 'none' doc.documentElement.appendChild(messagingIframe) }
對於(2)中的WebViewJavascriptBridge,其對象擁有以下方法:
window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromObjC: _handleMessageFromObjC }
方法的實現:
<span style="white-space:pre"> </span>/* *初始化方法,注入默認的消息處理器 *默認的消息處理器用於在處理來自objc的消息時,若是該消息沒有設置處理器,則採用默認處理器處理 */ function init(messageHandler) { if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') } WebViewJavascriptBridge._messageHandler = messageHandler var receivedMessages = receiveMessageQueue receiveMessageQueue = null //若是接收隊列有消息,則處理 for (var i=0; i<receivedMessages.length; i++) { _dispatchMessageFromObjC(receivedMessages[i]) } } <span style="white-space:pre"> </span>/* *發送消息並設置回調 */ function send(data, responseCallback) { _doSend({ data:data }, responseCallback) } /* *註冊消息處理器 */ function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler } /* *調用處理器並設置回調 */ function callHandler(handlerName, data, responseCallback) { _doSend({ data:data, handlerName:handlerName }, responseCallback) }
涉及到的兩個內部方法:
<span style="white-space:pre"> </span>/* *內部方法:消息的發送 */ function _doSend(message, responseCallback) { //若是定義了回調 if (responseCallback) { //爲回調對象產生惟一標識 var callbackId = 'js_cb_'+(uniqueId++) //並存儲到一個集合對象裏 responseCallbacks[callbackId] = responseCallback //新增一個key-value對- 'callbackId':callbackId message['callbackId'] = callbackId } sendMessageQueue.push(JSON.stringify(message)) messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE } <span style="white-space:pre"> </span>/* *內部方法:處理來自objc的消息 */ function _dispatchMessageFromObjC(messageJSON) { setTimeout(function _timeoutDispatchMessageFromObjC() { var message = JSON.parse(messageJSON) var messageHandler if (message.responseId) { //取出回調函數對象並執行 var responseCallback = responseCallbacks[message.responseId] responseCallback(message.error, message.responseData) delete responseCallbacks[message.responseId] } else { var response if (message.callbackId) { var callbackResponseId = message.callbackId response = { respondWith: function(responseData) { _doSend({ responseId:callbackResponseId, responseData:responseData }) }, respondWithError: function(error) { _doSend({ responseId:callbackResponseId, error:error }) } } } var handler = WebViewJavascriptBridge._messageHandler //若是消息中已包含消息處理器,則使用該處理器;不然使用默認處理器 if (message.handlerName) { handler = messageHandlers[message.handlerName] } try { handler(message.data, response) } catch(exception) { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception) } } }) }
還有兩個js方法是供native端直接調用的方法(它們自己也是爲native端服務的):
<span style="white-space:pre"> </span>/* *得到隊列,將隊列中的每一個元素用分隔符分隔以後連成一個字符串【native端調用】 */ function _fetchQueue() { var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR) sendMessageQueue = [] return messageQueueString } <span style="white-space:pre"> </span>/* *處理來自ObjC的消息【native端調用】 */ function _handleMessageFromObjC(messageJSON) { //若是接收隊列對象存在則入隊該消息,不然直接處理 if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON) } else { _dispatchMessageFromObjC(messageJSON) } }
最後還有一段代碼就是,定義一個事件並觸發,同時設置設置上面定義的WebViewJavascriptBridge對象爲事件的一個屬性:
<span style="white-space:pre"> </span>var doc = document _createQueueReadyIframe(doc) //建立並實例化一個事件對象 var readyEvent = doc.createEvent('Events') readyEvent.initEvent('WebViewJavascriptBridgeReady') readyEvent.bridge = WebViewJavascriptBridge //觸發事件 doc.dispatchEvent(readyEvent)
Native端實現
其實大體跟上面的相似,只是由於語法不一樣(因此我上面才說兩端是對等的):
WebViewJavascriptBridge.h/.m
它其實能夠看做UIWebView的Controller,實現了UIWebViewDelegate協議:
@interface WebViewJavascriptBridge : NSObject <UIWebViewDelegate> + (id)bridgeForWebView:(UIWebView*)webView handler:(WVJBHandler)handler; + (id)bridgeForWebView:(UIWebView*)webView webViewDelegate:(id <UIWebViewDelegate>)webViewDelegate handler:(WVJBHandler)handler; + (void)enableLogging; - (void)send:(id)message; - (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback; - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; - (void)callHandler:(NSString*)handlerName; - (void)callHandler:(NSString*)handlerName data:(id)data; - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; @end
方法的實現實際上是跟前面相似的,這裏咱們只看一下UIWebView的一個協議方法
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if (webView != _webView) { return YES; } NSURL *url = [request URL]; if ([[url scheme] isEqualToString:CUSTOM_PROTOCOL_SCHEME]) { //隊列中有數據 if ([[url host] isEqualToString:QUEUE_HAS_MESSAGE]) { //刷出隊列中數據 [self _flushMessageQueue]; } else { NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", CUSTOM_PROTOCOL_SCHEME, [url path]); } return NO; } else if (self.webViewDelegate) { return [self.webViewDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; } else { return YES; } }
使用示例
UI端
<span style="white-space:pre"> </span>//給WebViewJavascriptBridgeReady事件註冊一個Listener document.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false) <span style="white-space:pre"> </span>//事件的響應處理 function onBridgeReady(event) { var bridge = event.bridge var uniqueId = 1 <span style="white-space:pre"> </span>//日誌記錄 function log(message, data) { var log = document.getElementById('log') var el = document.createElement('div') el.className = 'logLine' el.innerHTML = uniqueId++ + '. ' + message + (data ? ': ' + JSON.stringify(data) : '') if (log.children.length) { log.insertBefore(el, log.children[0]) } else { log.appendChild(el) } } <span style="white-space:pre"> </span>//初始化操做,並定義默認的消息處理邏輯 bridge.init(function(message) { log('JS got a message', message) }) <span style="white-space:pre"> </span>//註冊一個名爲testJavascriptHandler的處理器,並定義用於響應的處理邏輯 bridge.registerHandler('testJavascriptHandler', function(data, response) { log('JS handler testJavascriptHandler was called', data) response.respondWith({ 'Javascript Says':'Right back atcha!' }) }) <span style="white-space:pre"> </span>//建立一個發送消息給native端的按鈕 var button = document.getElementById('buttons').appendChild(document.createElement('button')) button.innerHTML = 'Send message to ObjC' button.ontouchstart = function(e) { e.preventDefault() <span style="white-space:pre"> </span>//發送消息 bridge.send('Hello from JS button') } document.body.appendChild(document.createElement('br')) <span style="white-space:pre"> </span>//建立一個用於調用native端處理器的按鈕 var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button')) callbackButton.innerHTML = 'Fire testObjcCallback' callbackButton.ontouchstart = function(e) { e.preventDefault() log("Calling handler testObjcCallback") //調用名爲testObjcCallback的native端處理器,並傳遞參數,同時設置回調處理邏輯 bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('Got response from testObjcCallback', response) }) } }
Native端
//實例化一個webview並加入到window中去 UIWebView* webView = [[UIWebView alloc] initWithFrame:self.window.bounds]; [self.window addSubview:webView]; //啓用日誌記錄 [WebViewJavascriptBridge enableLogging]; //實例化WebViewJavascriptBridge並定義native端的默認消息處理器 _bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) { NSLog(@"ObjC received message from JS: %@", data); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }]; //註冊一個供UI端調用的名爲testObjcCallback的處理器,並定義用於響應的處理邏輯 [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponse *response) { NSLog(@"testObjcCallback called: %@", data); [response respondWith:@"Response from testObjcCallback"]; }]; //發送一條消息給UI端並定義回調處理邏輯 [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) { if (error) { NSLog(@"Uh oh - I got an error: %@", error); } NSLog(@"objc got response! %@ %@", error, responseData); }]; //調用一個在UI端定義的名爲testJavascriptHandler的處理器,沒有定義回調 [_bridge callHandler:@"testJavascriptHandler" data:[NSDictionary dictionaryWithObject:@"before ready" forKey:@"foo"]]; [self renderButtons:webView]; [self loadExamplePage:webView]; //單純發送一條消息給UI端 [_bridge send:@"A string sent from ObjC after Webview has loaded."];
附件:
/cms/uploads/soft/131230/4196-131230143008.zip |