優秀開源代碼解讀之JS與iOS Native Code互調的優雅實現方案

簡介

本篇爲大家介紹一個優秀的開源小項目:WebViewJavascriptBridge

它優雅地實現了在使用UIWebView時JS與ios 的ObjC nativecode之間的互調,支持消息發送、接收、消息處理器的註冊與調用以及設置消息處理的回調。

就像項目的名稱一樣,它是連接UIWebView和Javascript的bridge。在加入這個項目之後,他們之間的交互處理方式變得很友好。

在native code中跟UIWebView中的js交互的時候,像下面這樣:

  1. //發送一條消息給UI端並定義回調處理邏輯  
  2.  [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) {  
  3.         if (error) { NSLog(@"Uh oh - I got an error: %@", error); }  
  4.         NSLog(@"objc got response! %@ %@", error, responseData);  
  5.  }];  


而在UIWebView中的js跟native code交互的時候也變得很簡潔,比如在調用處理器的時候,就可以定義回調處理邏輯:

[javascript] view plaincopyprint?
  1. //調用名爲testObjcCallback的native端處理器,並傳遞參數,同時設置回調處理邏輯  
  2. bridge.callHandler('testObjcCallback', {'foo''bar'}, function(response) {  
  3. <span style="white-space:pre">  </span>log('Got response from testObjcCallback', response)  
  4. })  

一起來看看它的實現吧,它總共就包含了三個文件:

  1. WebViewJavascriptBridge.h  
  2. WebViewJavascriptBridge.m  
  3. WebViewJavascriptBridge.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),相應的代碼如下:

[javascript] view plaincopyprint?
  1. /* 
  2.  *創建一個iFrame,設置隱藏並加入到DOM中 
  3.  */  
  4.     function _createQueueReadyIframe(doc) {  
  5.         messagingIframe = doc.createElement('iframe')  
  6.         messagingIframe.style.display = 'none'  
  7.         doc.documentElement.appendChild(messagingIframe)  
  8.     }  

對於(2)中的WebViewJavascriptBridge,其對象擁有如下方法:

  1. window.WebViewJavascriptBridge = {  
  2.         init: init,  
  3.         send: send,  
  4.         registerHandler: registerHandler,  
  5.         callHandler: callHandler,  
  6.         _fetchQueue: _fetchQueue,  
  7.         _handleMessageFromObjC: _handleMessageFromObjC  
  8.     }  

方法的實現:

[javascript] view plaincopyprint?
  1. <span style="white-space:pre">  </span>/* 
  2.      *初始化方法,注入默認的消息處理器 
  3.      *默認的消息處理器用於在處理來自objc的消息時,如果該消息沒有設置處理器,則採用默認處理器處理 
  4.      */  
  5.     function init(messageHandler) {  
  6.         if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }  
  7.         WebViewJavascriptBridge._messageHandler = messageHandler  
  8.         var receivedMessages = receiveMessageQueue  
  9.         receiveMessageQueue = null  
  10.         //如果接收隊列有消息,則處理  
  11.         for (var i=0; i<receivedMessages.length; i++) {  
  12.             _dispatchMessageFromObjC(receivedMessages[i])  
  13.         }  
  14.     }  
  15.   
  16.   
  17. <span style="white-space:pre">  </span>/* 
  18.      *發送消息並設置回調 
  19.      */  
  20.     function send(data, responseCallback) {  
  21.         _doSend({ data:data }, responseCallback)  
  22.     }  
  23.       
  24.     /* 
  25.      *註冊消息處理器 
  26.      */  
  27.     function registerHandler(handlerName, handler) {  
  28.         messageHandlers[handlerName] = handler  
  29.     }  
  30.       
  31.   
  32.     /* 
  33.      *調用處理器並設置回調 
  34.      */  
  35.     function callHandler(handlerName, data, responseCallback) {  
  36.         _doSend({ data:data, handlerName:handlerName }, responseCallback)  
  37.     }  

涉及到的兩個內部方法:

[javascript] view plaincopyprint?
  1. <span style="white-space:pre">  </span>/* 
  2.      *內部方法:消息的發送 
  3.      */  
  4.     function _doSend(message, responseCallback) {  
  5.         //如果定義了回調  
  6.         if (responseCallback) {  
  7.             //爲回調對象產生唯一標識  
  8.             var callbackId = 'js_cb_'+(uniqueId++)  
  9.             //並存儲到一個集合對象裏  
  10.             responseCallbacks[callbackId] = responseCallback  
  11.             //新增一個key-value對- 'callbackId':callbackId  
  12.             message['callbackId'] = callbackId  
  13.         }  
  14.         sendMessageQueue.push(JSON.stringify(message))  
  15.         messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE  
  16.     }  
  17.   
  18.   
  19. <span style="white-space:pre">  </span>/* 
  20.      *內部方法:處理來自objc的消息 
  21.      */  
  22.     function _dispatchMessageFromObjC(messageJSON) {  
  23.         setTimeout(function _timeoutDispatchMessageFromObjC() {  
  24.             var message = JSON.parse(messageJSON)  
  25.             var messageHandler  
  26.               
  27.             if (message.responseId) {  
  28.                 //取出回調函數對象並執行  
  29.                 var responseCallback = responseCallbacks[message.responseId]  
  30.                 responseCallback(message.error, message.responseData)  
  31.                 delete responseCallbacks[message.responseId]  
  32.             } else {  
  33.                 var response  
  34.                 if (message.callbackId) {  
  35.                     var callbackResponseId = message.callbackId  
  36.                     response = {  
  37.                         respondWith: function(responseData) {  
  38.                             _doSend({ responseId:callbackResponseId, responseData:responseData })  
  39.                         },  
  40.                         respondWithError: function(error) {  
  41.                             _doSend({ responseId:callbackResponseId, error:error })  
  42.                         }  
  43.                     }  
  44.                 }  
  45.                   
  46.                 var handler = WebViewJavascriptBridge._messageHandler  
  47.                 //如果消息中已包含消息處理器,則使用該處理器;否則使用默認處理器  
  48.                 if (message.handlerName) {  
  49.                     handler = messageHandlers[message.handlerName]  
  50.                 }  
  51.                   
  52.                 try {  
  53.                     handler(message.data, response)  
  54.                 } catch(exception) {  
  55.                     console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)  
  56.                 }  
  57.             }  
  58.         })  
  59.     }  

還有兩個js方法是供native端直接調用的方法(它們本身也是爲native端服務的):

[javascript] view plaincopyprint?
  1. <span style="white-space:pre">  </span>/* 
  2.      *獲得隊列,將隊列中的每個元素用分隔符分隔之後連成一個字符串【native端調用】 
  3.      */  
  4.     function _fetchQueue() {  
  5.         var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR)  
  6.         sendMessageQueue = []  
  7.         return messageQueueString  
  8.     }  
  9.   
  10.   
  11. <span style="white-space:pre">  </span>/* 
  12.      *處理來自ObjC的消息【native端調用】 
  13.      */  
  14.     function _handleMessageFromObjC(messageJSON) {  
  15.         //如果接收隊列對象存在則入隊該消息,否則直接處理  
  16.         if (receiveMessageQueue) {  
  17.             receiveMessageQueue.push(messageJSON)  
  18.         } else {  
  19.             _dispatchMessageFromObjC(messageJSON)  
  20.         }  
  21.     }  

最後還有一段代碼就是,定義一個事件並觸發,同時設置設置上面定義的WebViewJavascriptBridge對象爲事件的一個屬性:

[javascript] view plaincopyprint?
  1. <span style="white-space:pre">  </span>var doc = document  
  2.     _createQueueReadyIframe(doc)  
  3.     //創建並實例化一個事件對象  
  4.     var readyEvent = doc.createEvent('Events')  
  5.     readyEvent.initEvent('WebViewJavascriptBridgeReady')  
  6.     readyEvent.bridge = WebViewJavascriptBridge  
  7.     //觸發事件  
  8.     doc.dispatchEvent(readyEvent)  

Native端實現

其實大致跟上面的類似,只是因爲語法不同(所以我上面才說兩端是對等的):

WebViewJavascriptBridge.h/.m

它其實可以看作UIWebView的Controller,實現了UIWebViewDelegate協議:

  1. @interface WebViewJavascriptBridge : NSObject <UIWebViewDelegate>  
  2. + (id)bridgeForWebView:(UIWebView*)webView handler:(WVJBHandler)handler;  
  3. + (id)bridgeForWebView:(UIWebView*)webView webViewDelegate:(id <UIWebViewDelegate>)webViewDelegate handler:(WVJBHandler)handler;  
  4. + (void)enableLogging;  
  5. - (void)send:(id)message;  
  6. - (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;  
  7. - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;  
  8. - (void)callHandler:(NSString*)handlerName;  
  9. - (void)callHandler:(NSString*)handlerName data:(id)data;  
  10. - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;  
  11. @end  

方法的實現其實是跟前面類似的,這裏我們只看一下UIWebView的一個協議方法

shouldStartLoadWithRequest:

  1. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {  
  2.     if (webView != _webView) { return YES; }  
  3.     NSURL *url = [request URL];  
  4.     if ([[url scheme] isEqualToString:CUSTOM_PROTOCOL_SCHEME]) {  
  5.         //隊列中有數據  
  6.         if ([[url host] isEqualToString:QUEUE_HAS_MESSAGE]) {  
  7.             //刷出隊列中數據  
  8.             [self _flushMessageQueue];  
  9.         } else {  
  10.             NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", CUSTOM_PROTOCOL_SCHEME, [url path]);  
  11.         }  
  12.         return NO;  
  13.     } else if (self.webViewDelegate) {  
  14.         return [self.webViewDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];  
  15.     } else {  
  16.         return YES;  
  17.     }  
  18. }  

使用示例

UI端

[javascript] view plaincopyprint?
  1. <span style="white-space:pre">  </span>//給WebViewJavascriptBridgeReady事件註冊一個Listener  
  2.     document.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false)  
  3.     <span style="white-space:pre">  </span>//事件的響應處理  
  4.     function onBridgeReady(event) {  
  5.         var bridge = event.bridge  
  6.         var uniqueId = 1  
  7.         <span style="white-space:pre">  </span>//日誌記錄  
  8.         function log(message, data) {  
  9.             var log = document.getElementById('log')  
  10.             var el = document.createElement('div')  
  11.             el.className = 'logLine'  
  12.             el.innerHTML = uniqueId++ + '. ' + message + (data ? ': ' + JSON.stringify(data) : '')  
  13.             if (log.children.length) { log.insertBefore(el, log.children[0]) }  
  14.             else { log.appendChild(el) }  
  15.         }  
  16.         <span style="white-space:pre">  </span>//初始化操作,並定義默認的消息處理邏輯  
  17.         bridge.init(function(message) {  
  18.             log('JS got a message', message)  
  19.         })  
  20.         <span style="white-space:pre">  </span>//註冊一個名爲testJavascriptHandler的處理器,並定義用於響應的處理邏輯  
  21.         bridge.registerHandler('testJavascriptHandler'function(data, response) {  
  22.             log('JS handler testJavascriptHandler was called', data)  
  23.             response.respondWith({ 'Javascript Says':'Right back atcha!' })  
  24.         })  
  25.   
  26.         <span style="white-space:pre">  </span>//創建一個發送消息給native端的按鈕  
  27.         var button = document.getElementById('buttons').appendChild(document.createElement('button'))  
  28.         button.innerHTML = 'Send message to ObjC'  
  29.         button.ontouchstart = function(e) {  
  30.             e.preventDefault()  
  31.             <span style="white-space:pre">      </span>//發送消息  
  32.             bridge.send('Hello from JS button')  
  33.         }  
  34.   
  35.         document.body.appendChild(document.createElement('br'))  
  36.   
  37.         <span style="white-space:pre">  </span>//創建一個用於調用native端處理器的按鈕  
  38.         var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))  
  39.         callbackButton.innerHTML = 'Fire testObjcCallback'  
  40.         callbackButton.ontouchstart = function(e) {  
  41.             e.preventDefault()  
  42.             log("Calling handler testObjcCallback")  
  43.             //調用名爲testObjcCallback的native端處理器,並傳遞參數,同時設置回調處理邏輯  
  44.             bridge.callHandler('testObjcCallback', {'foo''bar'}, function(response) {  
  45.                 log('Got response from testObjcCallback', response)  
  46.             })  
  47.         }  
  48.     }  

Native端

  1. //實例化一個webview並加入到window中去  
  2.     UIWebView* webView = [[UIWebView alloc] initWithFrame:self.window.bounds];  
  3.     [self.window addSubview:webView];  
  4.       
  5.     //啓用日誌記錄  
  6.     [WebViewJavascriptBridge enableLogging];  
  7.       
  8.     //實例化WebViewJavascriptBridge並定義native端的默認消息處理器  
  9.     _bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) {  
  10.         NSLog(@"ObjC received message from JS: %@", data);  
  11.         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];  
  12.         [alert show];  
  13.     }];  
  14.       
  15.     //註冊一個供UI端調用的名爲testObjcCallback的處理器,並定義用於響應的處理邏輯  
  16.     [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponse *response) {  
  17.         NSLog(@"testObjcCallback called: %@", data);  
  18.         [response respondWith:@"Response from testObjcCallback"];  
  19.     }];  
  20.       
  21.     //發送一條消息給UI端並定義回調處理邏輯  
  22.     [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) {  
  23.         if (error) { NSLog(@"Uh oh - I got an error: %@", error); }  
  24.         NSLog(@"objc got response! %@ %@", error, responseData);  
  25.     }];  
  26.       
  27.     //調用一個在UI端定義的名爲testJavascriptHandler的處理器,沒有定義回調  
  28.     [_bridge callHandler:@"testJavascriptHandler" data:[NSDictionary dictionaryWithObject:@"before ready" forKey:@"foo"]];  
  29.       
  30.     [self renderButtons:webView];  
  31.     [self loadExamplePage:webView];  
  32.       
  33.     //單純發送一條消息給UI端  
  34.     [_bridge send:@"A string sent from ObjC after Webview has loaded."];  

項目運行截圖:


 轉載自:http://blog.csdn.net/yanghua_kobe/article/details/8209751

發佈了34 篇原創文章 · 獲贊 20 · 訪問量 138萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章