WebViewJavascriptBridge原理(個人理解)

之前的工作很少跟h5交互打交道。最近公司新項目要和h5交互。oc調js 很簡單。就是js調OC比較麻煩。安卓的一句話搞定。我們如果用jscontent也很簡單。最后還是選擇了WebViewJavascriptBridge。這個框架。這個框架github上9.3k的星星。說明還是很厲害的。

下午,的時候請教了一下領導。幫我解釋了WebViewJavascriptBridge demo 里面的html里面的js代碼的意思。了解了一下js,貌似js入門還是很簡單的。于是很快的理解了這個框架。好了切入正題。

ExampleApp.html里面的js代碼:

window.onerror = function(err) {
        log('window.onerror: ' + err)
    }

    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)
    }

    setupWebViewJavascriptBridge(function(bridge) {
        var uniqueId = 1
        function log(message, data) {
            var log = document.getElementById('log')
            var el = document.createElement('div')
            el.className = 'logLine'
            el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
            if (log.children.length) { log.insertBefore(el, log.children[0]) }
            else { log.appendChild(el) }
        }

        bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
            log('ObjC called testJavascriptHandler with', data)
            var responseData = { 'Javascript Says':'Right back atcha!' }
            log('JS responding with', responseData)
            responseCallback(responseData)
        })

        document.body.appendChild(document.createElement('br'))

        var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
        callbackButton.innerHTML = 'Fire testObjcCallback'
        callbackButton.onclick = function(e) {
            e.preventDefault()
            log('JS calling handler "testObjcCallback"')
            bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS got response', response)
            })
        }
    })

function setupWebViewJavascriptBridge(callback) {.....}是定義一個函數。
而下面直接調了這個函數:setupWebViewJavascriptBridge(.....),里面參數是一個函數,setupWebViewJavascriptBridge函數會先判斷window.WebViewJavascriptBridge這個對象是否存在,有的話就直接返回了。貌似下面使用來創建window.WebViewJavascriptBridge對象的。第一次肯定是沒有的。那么久往下看:

window.WVJBCallbacks = [callback];

在window對象創建一個WVJBCallbacks屬性,其實后面[callback]告訴我們WVJBCallbacks是一個數組,callback就是執行setupWebViewJavascriptBridge(.....)傳進來的 函數.接著創建了一個frame: WVJBIframe,又把WVJBIframe .src設置一個url:

WVJBIframe.src = 'https://__bridge_loaded__

這樣操作后果就是網頁會請求https://bridge_loaded這個鏈接。到此 HTML里面的js解釋完畢。去看看網頁拿這個url干什么了。

我們注意到 這個要執行這個:

+ (instancetype)bridgeForWebView:(id)webView

我們看看這個里面干什么了。

+ (instancetype)bridgeForWebView:(id)webView {
    return [self bridge:webView];
}
+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
    if ([webView isKindOfClass:[WKWebView class]]) {
        return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
    }
#endif
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
    return nil;
}

//地方代碼
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.policyDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

看完了解到其實吧UIwebView delegate設置為WebViewJavascriptBridge對象。也就是說剛剛https://bridge_loaded URL加載情況會被WebViewJavascriptBridge攔截。我們去看看。

- (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 isWebViewJavascriptBridgeURL: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;
    }
}

最后發現:

if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        }
//其他地方代碼
#define kBridgeLoaded      @"__bridge_loaded__"

- (BOOL)isBridgeLoadedURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}

這地地方會攔截剛才那個Url。
看了injectJavascriptFile:

- (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];
        }
    }
}

其實最最重要的是注入一個js文件;

Snip20170904_1.png

稍微懂點js語法得人都知道。這里面創建了window.WebViewJavascriptBridge這個對象。以及這個對象的屬性等等。稍微細心的同學護法在126行他執行了一個函數_callWVJBCallbacks,緊接著線面就是這個函數的實現:

function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

看到了callbacks = window.WVJBCallbacks;然后下面循環執行。我們上面window.WVJBCallbacks 里面放的是setupWebViewJavascriptBridge(....)執行里面參數,這個參數是個函數。那么這個函數參數就會被這個循環執行,這個參數函數的參數就是WebViewJavascriptBridge(等同window.WebViewJavascriptBridge)。看看這個這個參數函數干了什么:

setupWebViewJavascriptBridge(function(bridge) {
        var uniqueId = 1
        function log(message, data) {
            var log = document.getElementById('log')
            var el = document.createElement('div')
            el.className = 'logLine'
            el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
            if (log.children.length) { log.insertBefore(el, log.children[0]) }
            else { log.appendChild(el) }
        }

        bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
            log('ObjC called testJavascriptHandler with', data)
            var responseData = { 'Javascript Says':'Right back atcha!' }
            log('JS responding with', responseData)
            responseCallback(responseData)
        })

        document.body.appendChild(document.createElement('br'))

        var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
        callbackButton.innerHTML = 'Fire testObjcCallback'
        callbackButton.onclick = function(e) {
            e.preventDefault()
            log('JS calling handler "testObjcCallback"')
            bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS got response', response)
            })

這個函數里面我們就叫他WebViewJavascriptBridge了,不在用bridge了。因為我已經知道這個參數就是WebViewJavascriptBridge。
WebViewJavascriptBridge調用了registerHandler,其實就是一個方法名對應一個js函數。是oc調js用的。不在具體分析的。
重點是下面的:

E83A996A-EAF6-4683-9B1F-F91955BEEC95.png

我們發現阿牛單擊就會讓WebViewJavascriptBridge調用callHandler;

function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
    
    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;
    }

_doSend里面又用了一個frame,并設置了一個url.這個URL就是https://wvjb_queue_message.這樣又出發請求Url。又回到WebViewJavascriptBridge攔截請求了。
最后觸發這個判斷:

WX20170904-012603@2x.png

messageQueueString里面包含了handerName和對應的blcok回調參數的值。具體看flushMessageQueue這個方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容