簡介
本篇為大家介紹一個優秀的開源小項目:WebViewJavascriptBridge。
它優雅地實現了在使用UIWebView時JS與ios 的ObjC nativecode之間的互調,支援訊息發送、接收、訊息處理器的註冊與調用以及設定訊息處理的回調。
就像項目的名稱一樣,它是串連UIWebView和Javascript的bridge。在加入這個項目之後,他們之間的互動處理方式變得很友好。
在native code中跟UIWebView中的js互動的時候,像下面這樣:
//發送一條訊息給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互動的時候也變得很簡潔,比如在調用處理器的時候,就可以定義回調處理邏輯:
//調用名為testObjcCallback的native端處理器,並傳遞參數,同時設定回調處理邏輯bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {log('Got response from testObjcCallback', response)})
一起來看看它的實現吧,它總共就包含了三個檔案:
WebViewJavascriptBridge.hWebViewJavascriptBridge.mWebViewJavascriptBridge.js.txt
它們是以如下的模式進行互動的:
很明顯:WebViewJavascriptBridge.js.txt主要用於銜接UIWebView中的web page,而WebViewJavascriptBridge.h/m則主要用於與ObjC的native code打交道。他們作為一個整體,其實起到了一個“橋樑”的作用,這三個檔案封裝了他們具體的互動處理方式,只開放出一些對外的涉及到業務處理的API,因此你在需要UIWebView與Native code互動的時候,引入該庫,則無需考慮太多的互動上的問題。整個的Bridge對你來說都是透明的,你感覺編程的時候,就像是web編程的前端和後端一樣清晰。
簡單地羅列一下它可以實現哪些功能吧:
出於表達上的需要,對於UIWebView相關的我就稱之為UI端,而objc那端的處理代碼稱之為Native端。
【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}
方法的實現:
/* *初始化方法,注入預設的訊息處理器 *預設的訊息處理器用於在處理來自objc的訊息時,如果該訊息沒有設定處理器,則採用預設處理器處理 */function init(messageHandler) {if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }WebViewJavascriptBridge._messageHandler = messageHandlervar receivedMessages = receiveMessageQueuereceiveMessageQueue = null//如果接收隊列有訊息,則處理for (var i=0; i<receivedMessages.length; i++) {_dispatchMessageFromObjC(receivedMessages[i])}}/* *發送訊息並設定回調 */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)}
涉及到的兩個內部方法:
/* *內部方法:訊息的發送 */function _doSend(message, responseCallback) {//如果定義了回調if (responseCallback) {//為回調對象產生唯一標識var callbackId = 'js_cb_'+(uniqueId++)//並儲存到一個集合對象裡responseCallbacks[callbackId] = responseCallback//新增一個key-value對- 'callbackId':callbackIdmessage['callbackId'] = callbackId}sendMessageQueue.push(JSON.stringify(message))messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE}/* *內部方法:處理來自objc的訊息 */function _dispatchMessageFromObjC(messageJSON) {setTimeout(function _timeoutDispatchMessageFromObjC() {var message = JSON.parse(messageJSON)var messageHandlerif (message.responseId) {//取出回呼函數對象並執行var responseCallback = responseCallbacks[message.responseId]responseCallback(message.error, message.responseData)delete responseCallbacks[message.responseId]} else {var responseif (message.callbackId) {var callbackResponseId = message.callbackIdresponse = {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端服務的):
/* *獲得隊列,將隊列中的每個元素用分隔字元分隔之後連成一個字串【native端調用】 */function _fetchQueue() {var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR)sendMessageQueue = []return messageQueueString}/* *處理來自ObjC的訊息【native端調用】 */function _handleMessageFromObjC(messageJSON) {//如果接收隊列對象存在則入隊該訊息,否則直接處理if (receiveMessageQueue) {receiveMessageQueue.push(messageJSON)} else {_dispatchMessageFromObjC(messageJSON)}}
最後還有一段代碼就是,定義一個事件並觸發,同時設定設定上面定義的WebViewJavascriptBridge對象為事件的一個屬性:
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的一個協議方法
shouldStartLoadWithRequest:
- (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端
//給WebViewJavascriptBridgeReady事件註冊一個Listenerdocument.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false) //事件的響應處理function onBridgeReady(event) {var bridge = event.bridgevar uniqueId = 1 //日誌記錄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) }} //初始化操作,並定義預設的訊息處理邏輯bridge.init(function(message) {log('JS got a message', message)}) //註冊一個名為testJavascriptHandler的處理器,並定義用於響應的處理邏輯bridge.registerHandler('testJavascriptHandler', function(data, response) {log('JS handler testJavascriptHandler was called', data)response.respondWith({ 'Javascript Says':'Right back atcha!' })}) //建立一個發送訊息給native端的按鈕var button = document.getElementById('buttons').appendChild(document.createElement('button'))button.innerHTML = 'Send message to ObjC'button.ontouchstart = function(e) {e.preventDefault() //發送訊息bridge.send('Hello from JS button')}document.body.appendChild(document.createElement('br')) //建立一個用於調用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."];
項目運行: