WebViewJavaScriptBridge深刻剖析

前言

前一篇文章中,咱們大體的講述了一下JavaScriptCore這個庫在iOS開發中的應用。在文中最後的階段,咱們提到了WebViewJavaScriptBridge這個庫。提到這個庫,可能有一些人就要說了,如今都什麼時代了,誰還會用這個庫啊?全是坑!不錯,早在三年前,這個庫有過一段輝煌的時光,在蘋果除了WKWebView以後,漸漸的使用這個庫的人愈來愈少,儘管這個庫也是支持了WKWebView的。 可是一個事物的存在就有他的價值,就算使用也不是那麼頻繁了,儘管他有不少的坑。可是對於一個開發者來講,咱們應該取其精華去其糟粕,現現在出的不少的交互的bridge依舊是有部分交互邏輯沿用了WebViewJavaScriptBridge的思想。 這裏就不得不提味精大神的一片文章,這篇文章裏面深刻淺出的談了談現現在Hybrid開發時經常使用的一些橋方法。有興趣的能夠去關注一下。廢話很少說,那麼咱們今天就從源碼開始解析這個庫的使用以及原理。javascript

簡介

簡單的來講,在最開始的UIWebView時,原生跟JS之間的交互通常是兩種方式:java

  • Native -> JS:這種方式很簡單,只是是原生調用stringByEvaluatingJavaScriptFromString:方法,傳入要執行的JS代碼就能夠實現;
  • JS -> Native:這種方式是在網頁上面加載一串Custom URL Scheme的URL,而後經過原生去UIWebView的代理方法webView:shouldStartLoadWithRequest:navigationType:中攔截相應的URL作處理。

固然這個其實也就是WebViewJavaScriptBridge的理論核心。可是上面這種實現方法爲何沒有人使用呢?緣由就是,經過在代理方法裏面攔截,咱們就必不可少的要寫不少的if else的代碼。在項目中的混合插件愈來愈多的時候,就致使了這個代理方法裏面的邏輯愈來愈臃腫,愈來愈難以維護。 那麼WebViewJavaScriptBridge的做用就是以更加優雅的方式,去實現Native與JS之間的互調。讓Native能像調用OC的方法同樣調用JS,同時JS也能像調用JS方法同樣去調用OC。這就在OC和JS中間搭起了一座友誼的橋樑。git

使用

這裏使用我就很少說了,直接pod 'WebViewJavascriptBridge'就能夠引入到項目了。 附上源碼地址:WebViewJavaScriptBridgegithub

目錄結構

WebViewJavaScriptBridge目錄結構

  • WebViewJavaScriptBridgeBase:bridge的核心類,用來初始化以及消息的處理;
  • WebViewJavaScriptBridge:判斷WebView的類型,並經過不一樣的類型進行分發。針對UIWebView和WebView作的一層封裝,主要歷來執行JS代碼,以及實現UIWebView和WebView的代理方法,並經過攔截URL來通知WebViewJavaScriptBridgeBase作的相應操做;
  • WKWebViewJavaScriptBridge:主要是針對WKWebView作的一些封裝,主要也是執行JS代碼和實現WKWebView的代理方法的。同上面這個類相似;
  • WebViewJavaScriptBridge_JS:裏面主要寫了一些JS的方法,JS端與Native」互動「的JS端的方法基 本上都在這個裏面;

主要流程

WebViewJavaScriptBridge參與交互的流程包括三個部分:初始化、JS調用Native、Native調用JS。接下來咱們就一一分析其中的過程。web

一、初始化

這裏必需要說一下,WebViewJavaScriptBridge的這個設計很巧妙,他在JS端和Native端,都各自初始化了一個WebViewJavaScriptBridge對象,就像是兩邊各自安排了一個」通信兵「,讓這兩個對象去完成消息的收發工做。同時兩邊還各自維護一個管理相應事件的messageHandlers容器、一個管理回調的callbackId容器。因此這裏的初始化,咱們得分爲兩個部分的初始化,一個部分是Native端的初始化,一個是JS端的初始化。這裏咱們都以UIWebView爲例子講解,WKWebView其實也是相相似的原理,能夠類比一下。json

(1)、Native端的初始化
  • 首先初始化WebViewJavaScriptBridge而且設置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
複製代碼
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
複製代碼

而後其內部初始化了WebViewJavaScriptBridgeBase類和相關的屬性bash

- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}
複製代碼
  • 註冊handler,這個handler是提供給JS調用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];
複製代碼

註冊其實就是在messageHandlers這個NSMutableDictionary裏面保存一下app

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
複製代碼
(2)、web view端的初始化
  • 當咱們經過loadRequest加載URL以後,網頁一加載就會執行網頁JS中的bridge的初始化方法setupWebViewJavascriptBridge函數
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 = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }
複製代碼

這裏主要作了兩件事情,一個是保存要執行的一直自定義初始化函數,好比註冊JS中的handler,第二個就是經過添加一個iframe加載初始化連接https://__bridge_loaded__函數

  • Native端會攔截https://__bridge_loaded__這個URL

WebViewJavaScriptBridge類中的攔截方法

  • 在webview中執行本地WebViewJavaScriptBridge_JS中的代碼,初始化window.WebViewJavaScriptBridge對象:首先在JS中建立一個WebViewJavaScriptBridge對象,設置成window一個屬性,而後定義幾個用於管理消息的全局變量,接着給WebViewJavaScriptBridge對象定義幾個處理消息的方法和函數,執行Native端startupMessageQueue中保存的消息,也就是本地JS文件還未加載時就發送了的消息。
window.WebViewJavascriptBridge = {
		registerHandler: registerHandler,
		callHandler: callHandler,
		disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	};
複製代碼

二、JS調用Native

  • JS中調用callHandler()方法,發消息給原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('JS got response', response)
			})
複製代碼

而後咱們看看callHandler是怎麼定義的學習

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

那麼這個_doSend是幹嗎的?咱們順着往下看

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;
	}
複製代碼

這下咱們清楚了,原來咱們在傳入handlerNamedata被包裝成了一個message傳入到_doSend函數,而後生成一個callbackId,也一道包裝到message中去。這樣三個數據都被打包成了一個message傳到Native。 固然爲何要傳入一個callbackId進去呢?這是由於用於處理原生回調的responseCallback是一個函數,是不能直接傳給原生的,因此這裏就把這個responseCallback存到了一個全局的responseCallbacks對象的屬性裏面去,屬性名就是responseCallback對應的id。這個地方就是爲了後面Native回調JS時,根據id找到對應的responseCallback

  • 在上圖中的最後一步指的是JS會在iframe中加載發送消息的URL,此時原生就能夠在相應的代理中攔截到這個URL,而後就知道JS端給我傳遞消息了,而後Native端會去調用JS,把sendMessageQueue中的message取出來,轉成JSON string的格式。接着原生把JSON string解析成字典,取出相應的datacallbackIdhandlerName。最後根據handlerName去先前的messageHanlers裏面取出相對應的block(handler),而後調用這個blockdata做爲第一個參數,第二個參數是根據callbackId建立的responseCallback(block),而後原生就能夠在block(handler)中處理接收到的data以及回調JS了。

  • 若是說須要原生給JS回調的話,當這個responseCallback被回調的時候,會執行下面的代碼

- (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];
}
複製代碼

這裏就是直接建立了一個message(NSMutableDictionary)對象,把datacallbackIdhandlerName封裝以後轉換成爲JSON string,最後調用WebViewJavascriptBridge._handleMessageFromObjC('%@')這個方法,把message傳給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];
        });
    }
}
複製代碼

在JS接收到了這個message以後,會根據裏面的callbackId找到以前的responseCallback,把data做爲參數,回調這個responseCallback

二、Native調用JS

其Native調用JS和上面JS調用Native是有不少的類似之處的。固然,其實也是能夠直接經過web view執行JS腳本去實現的。可是WebViewJavaScriptBridge使用了一套更加規範的調用方式。接下來來介紹一下這種方式。

  • Native調用callHandler()方法,把消息發送給JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
複製代碼

這個方法跟JS裏面的這個方法名是同樣的,固然實際的做用其實也是類似的。 在這裏都是將handlerNamedataresponseCallback對應的id包裝成一個message。而後把這個message對象轉成JSON string。最後在調用WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)方法把數據給到JS。這裏至於爲何也是傳id,其實原理跟上面是同樣的,block也是不能直接傳給JS的,因此這裏把responseCallback的這個block存到了全局的responseCallbacks字典裏面去了,key就是responseCallback對應的id。JS回調Native的時候,就會來這個字典裏面去取對應的block。其實思想都是差很少的。

  • JS端拿到了這個message以後,會將它解析成爲JS對象,而後去使用datacallbackIdhandlerName。而後根據handlerNamemessageHandlers裏面去對應的handler函數,而後去執行這個函數。第一個參數是傳過來的data,第二個參數就是根據callbackId建立的responseCallback的function。這裏就能夠在handler裏面處理接收到的回調了。
  • 這裏與前面JS調Native時Native回調JS的處理不太同樣,由於JS調Native是不能直接調的。可是怎麼去通知Native呢?其實他這裏就是直接走了JS調用Native的流程,就是上面提到的這個流程。不過仍是有不一樣的:
    • 一是message裏面的東西不同了;
    • 二是Native對message的處理:
      • 跟上面JS調用Native不同的就是message裏面如今不須要你傳一個callbackId了,由於這裏原本就是JS回調給Native的,再傳這個,兩邊就一直在回調來回調去了。可是呢,多了一個responseId,這是由於Native執行JS回調的時候,會根據這個responseIdresponseCallbacks中去取對應的block

        ```
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        ```
        複製代碼
        • Native在收到JS回調以後,會根據responseId找到以前保存的responseCallback的block,而後把message中的responseData(其實就是data)做爲參數回調給這個responseCallback。與JS調用Native不一樣的其實就是這裏的responseCallback只有一個data參數了,是沒有用於再次回調JS的block了。

總結

至此,WebViewJavaScriptBridge的總體核心流程就基本上講完了。這樣看看,其實其中的原理還算是簡單,可是很巧妙。兩邊都維護了一個WebViewJavaScriptBridge的對象,消息都封裝成爲一個message,而後全部的callback,都巧妙的轉換成了id。經過直接傳遞id,而後根據id分別去對應的地方去尋找到對應的callback。這種方式,其實也是值得咱們去學習和使用的。 接下來我會繼續的去研究如今比較火爆的JSCore的交互方式,對於Hybrid開發有想法的朋友,歡迎留言跟我交流。

相關文章
相關標籤/搜索