WebViewJavaScriptBridge深入剖析

原文作者:CoderSpr1ngHall
原文地址:https://juejin.im/post/5cecd746e51d45778f076cac

前言

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

簡介

簡單的來說,在最開始的UIWebView時,原生跟JS之間的交互一般是兩種方式:

  • 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中間搭起了一座友誼的橋樑。

使用

這裏使用我就不多說了,直接pod 'WebViewJavascriptBridge'就可以引入到項目了。 附上源碼地址: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。接下來我們就一一分析其中的過程。

1、初始化

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

(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類和相關的屬性

- (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裏面保存一下

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

2、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

2、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開發有想法的朋友,歡迎留言跟我交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章