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

來源:互聯網
上載者:User
簡介

本篇為大家介紹一個優秀的開源小項目: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."];

項目運行:

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.