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

本篇爲你們介紹一個優秀的開源小項目:WebViewJavascriptBridgejavascript

 

它優雅地實現了在使用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
相關文章
相關標籤/搜索