OC與JS交互之WebViewJavascriptBridge


 

 

上一篇文章介紹了通過UIWebView實現了OC與JS交互的可能性及實現的原理,並且簡單的實現了一個小的示例DEMO,當然也有一部分遺留問題,使用原生實現過程比較繁瑣,代碼難以維護。這篇文章主要介紹下開源庫WebViewJavascriptBridge的實現原理和使用方法,並用此開源庫重寫之前的示例,同樣,本文的示例代碼我會在文章后面給出歡迎star

 

我們在上一篇文章結尾處簡要介紹了WebViewJavascriptBridge的實現原理也是基於UIWebView的協議攔截,通過閱讀源碼發現,中間有很多地方是值得學習的。涉計到消息派遣,數據序列化,異常處理等技術思想

 

一、WebViewJavascriptBridge的實現原理

WebViewJavascriptBridge如其名字定義,就相當於一座橋梁,兩端連接了Obj-C和JavaScript。它提供了OC和JS互調的方法接口,方法在互調之前,我們需要向對方注冊我們的方法列表

1. OC調用和回調JS

//1.1 JS注冊OC的方法並實現回調OC
bridge.registerHandler('JS Echo', function(data, responseCallback) {
  console.log("JS Echo called with:", data)
  responseCallback(data)
})

  

//1.2 OC調用JS方法並實現回調函數
[self.bridge callHandler:@"JS Echo" responseCallback:^(id responseData) {
    NSLog(@"ObjC received response: %@", responseData);
}];

  

2. JS調用和回調OC

//2.1 OC注冊JS的方法並實現回調JS
[self.bridge registerHandler:@"ObjC Echo" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"ObjC Echo called with: %@", data);
    responseCallback(data);
}];

  

//2.2 JS調用OC方法並實現回調函數
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
  console.log("JS received response:", responseData)
})

  

首先看一張模型圖(圖片來自網絡)

透過模型圖,我們可以清楚的看到WebViewJavascriptBridge充當的橋梁是如何工作的

具體用法,參考官方的使用指南

下面我們我們來具體分析WebViewJavascriptBridge實現原理:

先看下庫的文件,一共有8個文件,而我們關心的文件只有3個,因為文件5,6是兼容WKWebView,實現原理都是一樣的,不需要看,所有的.h文件是聲明.m中的方法列表也不需要看

1. WebViewJavascriptBridge.h  
2. WebViewJavascriptBridge.m 
3. WebViewJavascriptBridgeBase.h  
4. WebViewJavascriptBridgeBase.m 
5. WKWebViewJavascriptBridge.h  
6. WKWebViewJavascriptBridge.m  
7. WebViewJavascriptBridge_JS.h  
8. WebViewJavascriptBridge_JS.m

接下來我們需要重點關注文件2,4,8中的代碼,值得一提的是文件7,8是 注入JS 相關的文件,之前JS代碼是放在一個叫WebViewJavascriptBridge.js.txt的文件,換掉的原因可以在文件8的頭部找到相關蹤跡,因為大概是和使用者的庫及其封裝有影響。接下來查看源碼的初始化部分:

1. 首先我們有一個bridge對象

@property (nonatomic, strong) WebViewJavascriptBridge *bridge;

2. 然后對齊進行初始化,初始化的過程中我們傳入了當前的webView對象,bridge在初始化(文件2)的過程中,把自己作為webView的代理對象,同時設置對應平台的代理(iOS/OSX,這里兼容了兩個平台),這樣在webView加載的時候,bridge就可以獲取到相關方法的調用和處理。至此我們完成了OC端bridge的初始化

self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];

3. bridge在初始化的時候會觸發bridgeBase的初始化(文件4),實際上消息的處理和回調都是在bridgeBase中完成的,bridge的作用是:進行webView代理的傳遞,對應平台初始化,調用bridgeBase中消息的注冊,調用和回調處理,我們來看下bridgeBase的頭文件中的屬性,有消息隊列數組,消息處理字典,響應回調字典和具體的消息處理對象

@property (assign) id <WebViewJavascriptBridgeBaseDelegate> delegate;
@property (strong, nonatomic) NSMutableArray* startupMessageQueue;
@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
@property (strong, nonatomic) WVJBHandler messageHandler;

4. 當webView第一次加載的時候,我們看下面的代碼,首先判斷是否是當前scheme,之后判斷URL是否是第一次加載的URL,與消息隊列有關的URL定義與第一次加載的URL定義和處理方式是不同的,所以這里要進行區分,第一次加載的時候我們會注入(文件8)中的相關JS代碼,后面具體介紹(文件8)的代碼。消息隊列相關的URL我們需要對消息進行響應處理

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    if ([_base isCorrectProcotocolScheme:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        } else {
            [_base logUnkownMessage:url];
        }
        return NO;
    } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

5. 第一次加載的時候我們會注入JS代碼,執行"_evaluateJavascript:"方法,實際調用的是webView中的"stringByEvaluatingJavaScriptFromString:"方法來注入JS代碼,注入之后如果此時隊列消息有消息的話就會進行消息的調用

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

6. 接下來我們看下JS端的部分代碼,注入的這部分JS代碼首先為html的window增加了一個bridge對象,這個對象里面有具體的有消息隊列數組,消息處理字典,響應回調字典和具體的消息處理對象,和OC端的bridge是保持一致的,到這里完成了JS部分的初始化,整個初始化也就完成了

if (window.WebViewJavascriptBridge) {
    return;
}
window.WebViewJavascriptBridge = {
    registerHandler: registerHandler,
    callHandler: callHandler,
    _fetchQueue: _fetchQueue,
    _handleMessageFromObjC: _handleMessageFromObjC
};

var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};

 

我們沿着OC調用JS方法和回調這條線路,繼續查看源碼:

7. 前文提到了OC調用JS方法,首先需要在JS端進行方法的注冊,我們來看方法的注冊代碼,函數定義部分是不變的,只要使用這個庫,那么這一部分代碼就是必須有的,調用函數傳入回調部分的代碼就是我們自己需要自定義的地方,這里的registerHandler中的函數名就是OC要調用JS的方法名

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
     //1 注冊JS的方法給OC
     bridge.registerHandler('JS Echo', function(data, responseCallback) {
     console.log("JS Echo called with:", data)  responseCallback(data) }) })

8. OC調用的時候callHandler的時候最終來到了bridgeBase中的下面的方法,首先對這一次調用進行封裝,並在內部緩存回調方法,然后進行消息的派遣

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    
    if (data) {
        message[@"data"] = data;
    }
    
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];
        message[@"callbackId"] = callbackId;
    }
    
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

9. 拿到對應的消息之后,首先要序列化成字符串對象,因為webView執行JavaScript腳本的時候接受的是一個字符串對象,之后進行一些列的轉換處理,在下面的代碼我們注意到有這樣的字符串"WebViewJavascriptBridge._handleMessageFromObjC",這個就是我們之前注入的JS腳本里面對OC方法的處理的函數,到這里我們成功的把OC的消息傳遞給了JS

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

10. JS拿到這個消息之后,進行消息的派遣,首先要反序列化字符串得到,方法名和參數,及方法的標識符,需要一提的是,這里先要檢查JS端緩存的的回調方法,通過標識符,我們可以找到對應的回調函數,如果有則調用方法,沒有則直接調用方法,下面的代碼就是這樣的過程,至此OC調用JS完成

function _dispatchMessageFromObjC(messageJSON) {
    setTimeout(function _timeoutDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                responseCallback = function(responseData) {
                    _doSend({ responseId:callbackResponseId, responseData:responseData });
                };
            }
            
            var handler = messageHandlers[message.handlerName];
            try {
                handler(message.data, responseCallback);
            } catch(exception) {
                console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
            }
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            }
        }
    });
}

  

接下來我們沿着JS調用OC方法和回調這條線路,查看源碼:

11. 同樣我們需要在OC端初始化之后,進行方法的注冊(前面提到過),注冊之后我們調用JS的callHandler,callHandler的時候進行回調函數的緩存,然后往消息隊列放入消息,之后重定向觸發OC的代碼調用

function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
} 

12. 前面也提到,重定向第一次是JS代碼的注入。下面是關於消息隊列的處理,首先檢查消息隊列字符串,然后反序列化字符串為json對象,這里反序列化之后的結果是一個包含數組的字典(鍵值對),之后就是根據消息的標識符,進行派發,如果對應的消息有回調函數的信息,那么會進行回調函數的調用

- (void)flushMessageQueue:(NSString *)messageQueueString{
    if (messageQueueString == nil || messageQueueString.length == 0) {
        NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
        return;
    }

    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        if (![message isKindOfClass:[WVJBMessage class]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];
        
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            handler(message[@"data"], responseCallback);
        }
    }
}

13. 這里我們也注意到了JS事件在重定向的時候並沒有在URL后面拼接相關字符串,那么上一步中的消息隊列字符串是怎么來的,查看源碼,執行這一步之前我們還調用了一個方法,我們又執行了一段JS腳本,這段JS腳本最終調用了fetchQueue,fetchQueue拼接了緩存在JS端的消息隊列,返回了消息隊列的字符串,這里和OC調JS是有區別的

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
//----------------------------------//
-(NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}

 

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
}

14. 到這里這個庫的基本原理已經解釋的很清楚了,通過閱讀源碼,可以學到很多高效的編碼方式,和編程思想,這里總結下整個過程:

a. OC端和JS端都有一個bridge對象,這個bridge對象處理管理者兩個端的交互方法

b. 每個bridge里面都有方法的注冊、調用和回調函數的緩存

c. 方法名和參數在傳遞的過程中會進行序列化和反序列化

d. 在調用方法的時候通過標識符來檢測回調函數並調用完成了整個過程

e. OC端事件觸發的關鍵是調用evaluatingJavaScript方法,JS端事件觸發的關鍵是window.location

 

二、使用WebViewJavascriptBridge重寫

了解了整個開源庫的原理寫起代碼來可謂是順風順水。這里用WebViewJavascriptBridge重寫了之前的那個示例,實現過程很簡單,下面貼出主要代碼

OC端:

//1 注冊OC的方法給JS
[self.bridge registerHandler:@"showMobile" handler:^(id data, WVJBResponseCallback responseCallback) {
    [self showMsg:@"我是下面的小紅 手機號是:18870707070"];
}];
[self.bridge registerHandler:@"showName" handler:^(id data, WVJBResponseCallback responseCallback) {
    
    NSString *info = [NSString stringWithFormat:@"你好 %@, 很高興見到你",data];
    [self showMsg:info];
}];
[self.bridge registerHandler:@"showSendMsg" handler:^(id data, WVJBResponseCallback responseCallback) {
    
    NSDictionary *dict = (NSDictionary *)data;
    NSString *info = [NSString stringWithFormat:@"這是我的手機號: %@, %@ !!",dict[@"mobile"],dict[@"events"]];
    
    [self showMsg:info];
}];

//2 調用JS注冊給OC的方法
- (IBAction)btnClick:(UIButton *)sender {
    if (sender.tag == 123) {
        [self.bridge callHandler:@"alertMobile"];
    }
    
    if (sender.tag == 234) {
        [self.bridge callHandler:@"alertName" data:@"小紅"];
    }
    
    if (sender.tag == 345) {
        [self.bridge callHandler:@"alertSendMsg" data:@{@"mobile":@"18870707070",@"events":@"周末爬山真是件愉快的事情"}];
    }
}

 

JS端:

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
     //1 注冊JS的方法給OC
     bridge.registerHandler('alertMobile', function(data, responseCallback) {
         alert('我是上面的小黃 手機號是:13300001111')
     })
     bridge.registerHandler('alertName', function(data, responseCallback) {
        alert('你好 ' + data + ', 我也很高興見到你')
           
     })
     bridge.registerHandler('alertSendMsg', function(data, responseCallback) {
         alert('這是我的手機號:' + data['mobile'] + ',' + data['events'] + '!!')
     })
})

//2 調用OC注冊給的方法JS
function btnClick1() {
    window.WebViewJavascriptBridge.callHandler('showMobile')
}
function btnClick2() {
    window.WebViewJavascriptBridge.callHandler('showMobile', '小黃')
    
}
function btnClick3() {
    window.WebViewJavascriptBridge.callHandler('showSendMsg', {'mobile': '13300001111', 'events':'周末一起去爬山'})
}

這里實現的是簡單的單向交互,因為之前設計的就比較簡單,重寫只完成了對應的功能,沒有回調模塊。回調的示例官方給的示例代碼就有,具體可以參考官方示例代碼

 

三、后記

iOS7之后,蘋果針對JavaScript出了一個官方的庫JavaScriptCore,是Objective-C封裝了WebKit的JavaScript引擎,使我們可以脫離WebView執行JS代碼。所以在iOS7之后想要實現交互,采用JavaScriptCore也是一種不錯的選擇,前提是你的項目不需要兼容到iOS7之前,我們將在下一篇文章介紹JavaScriptCore的組成及使用

 

戳這里:本文的DEMO地址歡迎star

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM