Native接H5框架WKWebViewJavascriptBridge原理總結(jié)

篇頭語

  • 本篇參考文章在章尾裆装,博客寫的很不錯(cuò)棠枉,但是需要反復(fù)詳讀加上自己在頭腦中繪制邏輯結(jié)構(gòu)才能比較好的理解,不然“東一塊挤悉、西一塊”很難聯(lián)系到一起,所以我結(jié)合了較多部分上述文章以外單獨(dú)又重新總結(jié)了一遍巫湘,加上了一些自己的理解和細(xì)節(jié)上的處理装悲,也做了一些方便理解的結(jié)構(gòu)圖,方便自己“一步到位”的復(fù)習(xí)尚氛,也方便其他人理解诀诊。
  • 至于為什么要選擇這個(gè)框架進(jìn)行解析呢,是因?yàn)槟壳癏5和Native的連接脫落不了“對(duì)于網(wǎng)址跳轉(zhuǎn)攔截”這一個(gè)過程阅嘶,此框架使用廣泛属瓣,寫法成熟,研讀此框架不僅是為了了解這個(gè)框架讯柔,更多意義上是為什么理解Native接H5的原理抡蛙,也方便了解各個(gè)框架對(duì)這方面的處理的差異化和優(yōu)勢(shì)體現(xiàn)在哪里

基本構(gòu)成

對(duì)于整個(gè)框架來說分為三個(gè)部分

框架包含的文件
  • oc部分,包括oc處理暴露給js接口的類:WKWebViewJavascriptBridge.m/.h
  • js部分磷杏,包括js處理暴露給oc接口的文件: ExampleApp.html
  • bridge處理部分溜畅,這個(gè)部分對(duì)于oc和js都各有一個(gè)文件,oc是類:WebViewJavascriptBridgeBase.m/.h极祸,js則是:WebViewJavascriptBridge_JS.m慈格,雖然是.m文件怠晴,但是其中是js代碼,點(diǎn)開就知道了浴捆。
  • oc和js部分的作用就是聲明給對(duì)方調(diào)用的方法蒜田,以及提供供自身使用可以調(diào)用對(duì)方的一個(gè)接口,但是具體如何調(diào)用的js选泻、如果調(diào)用的oc或者說如何注冊(cè)給js冲粤、如何注冊(cè)給oc使用的方法,這些邏輯都放在兩端的bridge部分進(jìn)行處理页眯。

H5端的處理過程

H5端初始化流程

處理過程其實(shí)是指對(duì)于bridge的處理過程梯捕,oc端的處理都在Base.m中,流程和js的初始化原理大致相同窝撵,但是js端的過程比較繁瑣傀顾,因?yàn)槌跏蓟^程直接就來了一個(gè)和oc的交互。為什么會(huì)有這么一個(gè)交互過程呢碌奉?因?yàn)閷?duì)于js的初始化代碼被存放到了移動(dòng)端的文件中短曾,這一點(diǎn)應(yīng)該是出于盡可能的減少雙端工作量的考量吧,這樣存放雖然加大了js端初始化的復(fù)雜性赐劣,但是這對(duì)于開發(fā)者是黑盒的嫉拐,所以也不能說是一個(gè)缺點(diǎn)。這個(gè)處理過程是一個(gè)在js端初始化一個(gè)bridge并達(dá)到通過這個(gè)bridge可以跟oc交互的目的的過程魁兼。先看一下植入WKWebViewJavascriptBridge框架的過程中婉徘,在js端所必須添加的代碼(官網(wǎng)要求復(fù)制進(jìn)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)
      }
    
      /*與OC交互的所有JS方法都要放在此處注冊(cè),才能調(diào)用通過JS調(diào)用OC或者讓OC調(diào)用這里的JS*/
      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)
         }
       }
       //register璃赡、call代碼
       bridge.registerHandler('getUserInfos', function(data, responseCallback) {
         log("這是在H5中的getUserInfos 接收的從ObjC傳過來的參數(shù)", data)
         responseCallback({'userId': '123456', 'blog': '標(biāo)哥的技術(shù)博客'})
       })
                                   
       /*JS給ObjC提供公開的API判哥,ObjC端通過注冊(cè),就可以在JS端調(diào)用此API時(shí)碉考,得到回調(diào)。ObjC端可以在處理完成后挺身,反饋給JS侯谁,這樣寫就是在載入頁面完成時(shí)就先調(diào)用*/
       bridge.callHandler('getUserIdFromObjC', function(responseData) {
         log("這是在H5中的getUserIdFromObjC方法對(duì)應(yīng)的參數(shù)responseData的值", responseData)
       })
       })

下面是對(duì)以上代碼進(jìn)行一個(gè)簡單的簡化,解耦出一個(gè)方法callback章钾,但是可以方便理解墙贱。所以主要看下面的代碼即可

function setupWebViewJavascriptBridge(callback) {
     //第一次調(diào)用這個(gè)方法的時(shí)候,為false
    if (window.WebViewJavascriptBridge) {
        var result = callback(WebViewJavascriptBridge);
        return result;
    }
    //第一次調(diào)用的時(shí)候贱傀,也是false
    if (window.WVJBCallbacks) {
        var result = window.WVJBCallbacks.push(callback);
        return result;
    }
    //把callback對(duì)象賦值給對(duì)象惨撇。
    window.WVJBCallbacks = [callback];
    //這段代碼的意思就是執(zhí)行加載WebViewJavascriptBridge_JS.js中代碼的作用
    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執(zhí)行的時(shí)候傳入的參數(shù),這是一個(gè)方法府寒。
function callback(bridge) {
    var uniqueId = 1
    //把WEB中要注冊(cè)的方法注冊(cè)到bridge里面
    bridge.registerHandler('OC調(diào)用JS提供的方法', function(data, responseCallback) {
        log('OC調(diào)用JS方法成功', data)
        var responseData = { 'JS給OC調(diào)用的回調(diào)':'回調(diào)值!' }
        log('OC調(diào)用JS的返回值', responseData)
        responseCallback(responseData)
    })
};
//驅(qū)動(dòng)所有hander的初始化
setupWebViewJavascriptBridge(callback);
  • 這里直接調(diào)用了setupWebViewJavascriptBridge這個(gè)方法魁衙,但是實(shí)際上調(diào)用這個(gè)方法前兩個(gè)語句都不會(huì)執(zhí)行报腔。因?yàn)闆]有初始化賦值的情況下,window的屬性都是空的剖淀,但是其他的語句是會(huì)被執(zhí)行的纯蛾,就是下面的代碼。
  • 單看這兩個(gè)方法setupWebViewJavascriptBridge以及callback來說的話纵隔,callback的參數(shù)bridge一直沒有被賦值翻诉,不過無所謂,在第一次調(diào)用的時(shí)候callback方法作為setupWebViewJavascriptBridge方法的參數(shù)捌刮,也沒有被調(diào)用碰煌,所以此時(shí)參數(shù)為空也無所謂。
  • 下面這段代碼的主要目的如下面注釋所說绅作,但是細(xì)節(jié)上也需要了解一下芦圾,(?js實(shí)在是沒什么基礎(chǔ),看起來比較吃力)代碼實(shí)際上創(chuàng)建了一個(gè)iframe棚蓄,這個(gè)是一個(gè)web中再打開一個(gè)web頁面的這么一個(gè)組件堕扶,把這個(gè)組件加到頁面又移走,設(shè)置為不可見梭依,然后這個(gè)iframe指向了一個(gè)地址為“https://bridge_loaded”的東西稍算,其實(shí)是為了制造webview的url跳轉(zhuǎn),從而觸發(fā)oc的回調(diào)方法役拴。
    _iframe可以理解為webview中的窗口糊探,當(dāng)我們改變iframe的src屬性的時(shí)候,相當(dāng)于我們?yōu)g覽器實(shí)現(xiàn)了鏈接的跳轉(zhuǎn)河闰。比如從www.baidu.com跳轉(zhuǎn)到www.google.com科平。下面這段代碼的目的就是實(shí)現(xiàn)一個(gè)到https://bridge_loaded的跳轉(zhuǎn)。觸發(fā)oc回調(diào)的目的是因?yàn)闉閖s端初始化bridge的代碼需要從oc端的回調(diào)方法中觸發(fā)姜性,從而達(dá)到初始化javascript環(huán)境的bridge的作用瞪慧。
//這段代碼的意思就是執(zhí)行加載WebViewJavascriptBridge_JS.js中代碼的作用
    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端初始化bridge的代碼是一個(gè)js文件,這個(gè)文件實(shí)現(xiàn)了為上述代碼中的callback方法中的bridge賦值部念。通過上述代碼在oc端被執(zhí)行在當(dāng)前webview弃酌,至于怎么通過這個(gè)“跳轉(zhuǎn)”觸發(fā)js文件的執(zhí)行的呢?這就取決于oc這邊的方法回調(diào)了儡炼,實(shí)現(xiàn)了WKNavigationDelegate協(xié)議的類會(huì)在內(nèi)部WKWebView發(fā)生地址跳轉(zhuǎn)的時(shí)候觸發(fā)以下這個(gè)方法:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    //如果是WebViewJavascriptBridge發(fā)送或者接受的消息妓湘,則特殊處理。否則按照正常流程處理乌询。
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        //第一次注入JS代碼榜贴,也就是說只有在初始化的時(shí)候才會(huì)被調(diào)用一次
        //是通過判斷url的scheme和host字段是不是預(yù)置的特殊含義的字段
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        //此處判斷出消息是從WEB發(fā)過來的,所有從web發(fā)來的消息都走此口妹田,無論是“js調(diào)用oc的方法”消息唬党,還是“oc調(diào)用js方法鹃共,js執(zhí)行完畢發(fā)回來的消息(目的是告訴oc我執(zhí)行完了,你可以執(zhí)行你設(shè)置的回調(diào)函數(shù)了3踵凇)”
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
    }
    //下面是webview的正常代理執(zhí)行流程及汉,不用管。
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

注意上面的代碼屯烦,很多是調(diào)用base的方法執(zhí)行坷随,這是解耦思想呦!和bridge相關(guān)的具體操作細(xì)節(jié)要移步到BridgeBase類實(shí)現(xiàn)驻龟。下面看一下[base injectJavascriptFile];這個(gè)方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且執(zhí)行温眉,從而達(dá)到初始化javascript環(huán)境的brige的作用。至于其他部分用到的時(shí)候再看翁狐。

//初始化的是否注入WebViewJavascriptBridge_JS.js
- (void)injectJavascriptFile {
    NSString *js = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WebViewJavascriptBridge_JS.js" ofType:nil] encoding:NSUTF8StringEncoding error:nil];
    //把javascript代碼注入webview中執(zhí)行,這里執(zhí)行具體的注入操作类溢。
    [self _evaluateJavascript:js];
    //如果javascript環(huán)境初始化完成以后,有startupMessageQueue消息露懒。則立即發(fā)送消息闯冷。
    //startupMessageQueue置為nil,不reset情況下將不會(huì)在被賦值
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
//把javascript代碼寫入webview
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

上面代碼注釋已經(jīng)很清晰了懈词,其實(shí)就是拼接一條js指令然后通過oc提供的方式把他丟到webview中去執(zhí)行蛇耀,然后看一下要執(zhí)行的這個(gè)文件,其實(shí)在下面的這段代碼目的執(zhí)行的js文件對(duì)于bridge進(jìn)行了賦值坎弯,下面來看一下這個(gè)WebViewJavascriptBridgeBase_JS.js文件纺涤,也就是js端的bridge文件

;(function() {
    //如果已經(jīng)初始化了,則返回抠忘。
    if (window.WebViewJavascriptBridge) {
        return;
    }
    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    //初始化一些屬性撩炊。
    var messagingIframe;
    //用于存儲(chǔ)消息列表
    var sendMessageQueue = [];
    //用于存儲(chǔ)消息
    var messageHandlers = {};
    //通過下面兩個(gè)協(xié)議組合來確定是否是特定的消息,然后攔擊崎脉。
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    //oc調(diào)用js的回調(diào)
    var responseCallbacks = {};
    //消息對(duì)應(yīng)的id
    var uniqueId = 1;
    //是否設(shè)置消息超時(shí)
    var dispatchMessagesWithTimeoutSafety = true;
    //web端注冊(cè)一個(gè)消息方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    //web端調(diào)用一個(gè)OC注冊(cè)的消息
    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;
    }
        //把消息轉(zhuǎn)換成JSON字符串返回
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    //OC調(diào)用JS的入口方法
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

    //初始化橋接對(duì)象拧咳,OC可以通過WebViewJavascriptBridge來調(diào)用JS里面的各種方法。
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };


    //處理從OC返回的消息囚灼。
    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
            _doDispatchMessageFromObjC();
        }

        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;
            //回調(diào)
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {//主動(dòng)調(diào)用
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                    };
                }
                //獲取JS注冊(cè)的函數(shù)
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //調(diào)用JS中的對(duì)應(yīng)函數(shù)處理
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    //把消息從JS發(fā)送到OC呛踊,執(zhí)行具體的發(fā)送操作。
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            //存儲(chǔ)消息的回調(diào)ID
            responseCallbacks[callbackId] = responseCallback;
            //把消息對(duì)應(yīng)的回調(diào)ID和消息一起發(fā)送啦撮,以供消息返回以后使用。
            message['callbackId'] = callbackId;
        }
        //把消息放入消息列表
        sendMessageQueue.push(message);
        //下面這句話會(huì)出發(fā)JS對(duì)OC的調(diào)用
        //讓webview執(zhí)行跳轉(zhuǎn)操作汪厨,從而可以在
        //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    //messagingIframe.body.style.backgroundColor="#0000ff";
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);


    //注冊(cè)_disableJavascriptAlertBoxSafetyTimeout方法赃春,讓OC可以關(guān)閉回調(diào)超時(shí),默認(rèn)是開啟的劫乱。
    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    //執(zhí)行_callWVJBCallbacks方法
    setTimeout(_callWVJBCallbacks, 0);

    //初始化WEB中注冊(cè)的方法织中。這個(gè)方法會(huì)把WEB中的hander注冊(cè)到bridge中锥涕。
    //下面的代碼其實(shí)就是執(zhí)行WEB中的callback函數(shù)。
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();

上面代碼比較復(fù)雜狭吼,但是卻比較容易看层坠,主要分為以下幾個(gè)功能:

  • 三個(gè)屬性:
    • sendMessageQueue 用于存儲(chǔ)消息列表
    • messageHandlers 用于存儲(chǔ)注冊(cè)給oc調(diào)用的方法
    • responseCallbacks 用于存儲(chǔ)oc在調(diào)用js方法時(shí)需要調(diào)用的js提供的回調(diào)方法
  • 實(shí)現(xiàn)了registerHandler方法,原理是把方法名和方法實(shí)現(xiàn)存儲(chǔ)在messageHandlers隊(duì)列中
  • 實(shí)現(xiàn)了callHandler方法刁笙,原理是生成一個(gè)callback的id破花,和callback的實(shí)現(xiàn)作為對(duì)用的key、value一同存儲(chǔ)在responseCallbacks隊(duì)列中疲吸,與此同時(shí)座每,把方法名和參數(shù)和callback的id存儲(chǔ)在一個(gè)字典中,然后吧這個(gè)字典加入到sendMessageQueue隊(duì)列中摘悴,最后變化網(wǎng)址峭梳,形成跳轉(zhuǎn),觸發(fā)oc回調(diào)方法蹂喻,實(shí)現(xiàn)一個(gè)信息交流葱椭,至于是怎么把參數(shù)和方法名稱帶過去的呢?
  • disableJavscriptAlertBoxSafetyTimeout等相關(guān)的一些列作用暫時(shí)沒研究口四?
  • 實(shí)現(xiàn)了接收oc消息的方法孵运,原理是解析收到的json值,從中解析出需要的回調(diào)函數(shù)的信息窃祝,然后觸發(fā)回調(diào)函數(shù)掐松,之后從回調(diào)函數(shù)的隊(duì)列responseCallbacks中刪除該回調(diào)方法,如果說oc返回的信息中沒有這個(gè)回調(diào)方法粪小,是不是說oc端沒有主動(dòng)回調(diào)這個(gè)js的回調(diào)大磺?則將會(huì)通過json數(shù)據(jù)獲取到之前存儲(chǔ)的message的callbackid來查找對(duì)應(yīng)的回調(diào)函數(shù)來進(jìn)行調(diào)用,之后的步驟和callHandler同理探膊,也就是說這個(gè)回調(diào)函數(shù)是一定會(huì)被調(diào)用的杠愧?
  • 通過同樣的方式制造跳轉(zhuǎn),目的是把初始化好之后把js要發(fā)給oc的消息一下發(fā)出去
  • 立即執(zhí)行的函數(shù)逞壁,把WebViewJavascriptBridge注入到之前文件的callbacks方法中作為參數(shù)流济,實(shí)現(xiàn)register方法的真正觸發(fā),(扣題)

oc環(huán)境初始化

在bridge.m中其實(shí)工作很簡單腌闯,他只是一個(gè)對(duì)于oc項(xiàng)目連接H5的入口,提供一個(gè)單例绳瘟,搞定自己的webview和base就好了。

//初始化一個(gè)OC環(huán)境的橋WKWebViewJavascriptBridge并且初始化姿骏。
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    //調(diào)用下面那個(gè)方法
    [bridge _setupInstance:webView];
    [bridge reset];
    return bridge;
}
//初始化
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

剩余的初始化工作在BridgeBase.m

//messageHandlers用于保存OC環(huán)境注冊(cè)的方法糖声,key是方法名,value是這個(gè)方法對(duì)應(yīng)的回調(diào)block
//startupMessageQueue用于保存是實(shí)話過程中需要發(fā)送給javascirpt環(huán)境的消息。
//responseCallbacks用于保存OC于javascript環(huán)境相互調(diào)用的回調(diào)模塊蘸泻。通過_uniqueId加上時(shí)間戳來確定每個(gè)調(diào)用的回調(diào)琉苇。
- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}

所有與javascript之間交互的信息都存儲(chǔ)在messageHandlers和responseCallbacks中。這兩個(gè)屬性記錄了OC環(huán)境與javascript交互的信息悦施。

OC發(fā)消息給WEB的過程梳理

oc向web發(fā)消息流程梳理圖

對(duì)于oc調(diào)用js的方法并扇,走下面這個(gè)流程:

- (void)callHandler:(id)sender {
    id data = @{ @"OC調(diào)用JS方法": @"OC調(diào)用JS方法的參數(shù)" };
    [_bridge callHandler:@"OC調(diào)用JS提供的方法" data:data responseCallback:^(id response) {
       // NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}
/*
    handerName:OC調(diào)用JS提供的方法
    data:{@"OC調(diào)用JS方法的參數(shù)":@"OC調(diào)用JS方法"}
    responseCallback:回調(diào)block
 */
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

把所有信息存入一個(gè)名字為message的字典中。里面拼裝好參數(shù)data抡诞、回調(diào)IDcallbackId(現(xiàn)場生成)穷蛹、要調(diào)用的js方法名字handlerName。于此同時(shí)沐绒,把剛剛生成的callbackID和callback方法存入自身的callback隊(duì)列中俩莽,目的是js執(zhí)行回來的時(shí)候再調(diào)用,具體如下乔遮,其實(shí)整個(gè)過程處理和js的bridge文件的處理方式一樣扮超,

- (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;
    }
    //注意是要調(diào)用的js方法的名字
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

其次把封裝好的信息[參數(shù)data、回調(diào)IDcallbackId(現(xiàn)場生成)蹋肮、要調(diào)用的js方法名字handlerName
]發(fā)送出去出刷,把OC消息序列化、并且轉(zhuǎn)化為javascript環(huán)境的格式坯辩。然后在主線程中調(diào)用_evaluateJavascript馁龟。

- (void)_queueMessage:(WVJBMessage *)message {
    //啥時(shí)候有啥時(shí)候沒有?漆魔?這里好像是只會(huì)存儲(chǔ)js初始化文件執(zhí)行之前的消息坷檩,由于js未初始化,所以消息都加到隊(duì)列中改抡,初始化之后以后一次集體發(fā)送矢炼,但是初始化之后隊(duì)列被置為nil,將不會(huì)再走if阿纤,都是有消息直接就發(fā)送出去
    if (self.startupMessageQueue) {
     //加完了就完事了嗎句灌??
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

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

- (NSString *)_serializeMessage:(id)message pretty:(BOOL)pretty {
    return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(pretty ? NSJSONWritingPrettyPrinted : 0) error:nil] encoding:NSUTF8StringEncoding];
}

整個(gè)流程對(duì)消息進(jìn)行了一系列處理欠拾,最后的處理通過一句代碼進(jìn)行發(fā)送

NSString *javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];

其實(shí)最終形成的字符串是

WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}');

其實(shí)就是通過javascript環(huán)境中的Bridge對(duì)象的_handleMessageFromObjC方法胰锌。下面我們?nèi)ebViewJavascriptBridge_JS.js中看_handleMessageFromObjC的處理過程。

//OC調(diào)用JS的入口方法
//messsgeJSON:{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }
    //處理從OC返回的消息藐窄。
function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
        _doDispatchMessageFromObjC();
    }

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        //很明顯在此過程中responseId)是沒有的资昧,所以跳過,message有三個(gè)key:data荆忍、callbackId榛搔、handleName
        //這一部分是oc執(zhí)行完js的調(diào)用返回信息調(diào)用js回調(diào)的邏輯
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
        //這一部分是處理oc調(diào)用js方法的邏輯
        //這里為什么要獲取oc的callbackID是因?yàn)橐趫?zhí)行完之后給oc發(fā)送消息時(shí)附帶
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                //準(zhǔn)備好方法結(jié)束后執(zhí)行的“通知oc可以執(zhí)行回調(diào)函數(shù)”的方法
                responseCallback = function(responseData) {
                   _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                };
            }
            //獲取JS注冊(cè)的函數(shù)
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //調(diào)用JS中的對(duì)應(yīng)函數(shù)處理诺凡,附帶上剛剛拼接好的回調(diào)函數(shù)
                handler(message.data, responseCallback);
            }
        }
    }
}

上面這段代碼很容易理解,而且我已經(jīng)把注釋寫的非常清晰了践惑,其實(shí)就是通過判斷消息中是否有responseId來判斷這是一個(gè)oc的回調(diào)消息,還是說oc想要調(diào)用js的方法嘶卧。js處理完方法調(diào)用之后直接調(diào)用_doSend方法把信息返回OC尔觉。下面我們看看_doSend的具體實(shí)現(xiàn):

//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作芥吟。
function _doSend(message, responseCallback) {
//在這一步驟中if語句是不執(zhí)行的
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //存儲(chǔ)消息的回調(diào)ID
        responseCallbacks[callbackId] = responseCallback;
        //把消息對(duì)應(yīng)的回調(diào)ID和消息一起發(fā)送侦铜,以供消息返回以后使用。
        message['callbackId'] = callbackId;
    }
    //把消息放入消息列表
    sendMessageQueue.push(message);
    //下面這句話會(huì)出發(fā)JS對(duì)OC的調(diào)用
    //讓webview執(zhí)行跳轉(zhuǎn)操作钟鸵,從而可以在
    //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

在這一過程中钉稍,并沒有向_doSend傳入responseCallback參數(shù),因?yàn)檫@是一個(gè)“js執(zhí)行完方法棺耍,要發(fā)送消息給oc”的過程贡未,而不是要發(fā)送“調(diào)用oc方法的消息”,所以不需要第二個(gè)參數(shù)蒙袍。也是就在if語句不執(zhí)行的情況下俊卤,直接通過頁面跳轉(zhuǎn)把消息發(fā)走,但是之前封裝的message并沒有隨著這個(gè)網(wǎng)址一起發(fā)走害幅,它被加入到了自身的sendMessageQueue隊(duì)列中消恍,等oc收到消息后會(huì)自己來取。
其中最重要還是最后面的通過改變iframe的messagingIframe.src以现。從而觸發(fā)webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler從而在OC中處理javascript環(huán)境觸發(fā)過來的回調(diào)狠怨。具體如下:

if ([_base isWebViewJavascriptBridgeURL:url]) {
    //第一次注入JS代碼
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    //處理WEB發(fā)過來的消息
    } else if ([_base isQueueMessageURL:url]) {
        [self WKFlushMessageQueue];
    } else {
        [_base logUnkownMessage:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
}

這里會(huì)走[self WKFlushMessageQueue];方法。然后通過調(diào)用WebViewJavascriptBridge._fetchQueue()來獲取javascript給OC的回調(diào)信息邑遏。

//獲取WEB消息的JSON字符串
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
////把消息或者WEB回調(diào)從OC發(fā)送到OC
- (void)WKFlushMessageQueue {
    NSString *js = [_base webViewJavascriptFetchQueyCommand];
    [_webView evaluateJavaScript:js completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        //把消息或者WEB回調(diào)從OC發(fā)送到OC
        [_base flushMessageQueue:result];
    }];
}

獲取到j(luò)avascript給OC的回調(diào)消息以后佣赖,然后把javascript的bridge返回的信息加工處理成OC環(huán)境的bridge能識(shí)別的信息。從而找到具體的實(shí)現(xiàn)執(zhí)行无宿。

//把從WEB發(fā)送的消息返回茵汰。然后在這里處理
- (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;
    }
   //此方法在上面有貼,用于解析json數(shù)據(jù)
    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);
        }
    }
}

其實(shí)就是依然是通過判斷消息中是否有responseId來判斷這是一個(gè)“js給oc的回調(diào)消息”孽鸡,還是說這是“js想調(diào)用oc的消息”蹂午。注意此時(shí)拿到的message是上一步在js中封裝的,其中包括data彬碱,callbackId和handleName

WEB發(fā)消息給OC

基于框架的作用豆胸,無論在oc端調(diào)用H5或者是在H5端調(diào)用oc都僅需含簡單的幾行代碼

bridge.callHandler('OC提供方法給JS調(diào)用',params, function(response) {
    log('JS調(diào)用OC的返回值', response)
})

此處調(diào)用的事BridgeBase.js中的方法

//web端調(diào)用一個(gè)OC注冊(cè)的消息
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName: handlerName, data: data }, responseCallback);
}
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作巷疼。
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //存儲(chǔ)消息的回調(diào)ID
        responseCallbacks[callbackId] = responseCallback;
        //把消息對(duì)應(yīng)的回調(diào)ID和消息一起發(fā)送晚胡,以供消息返回以后使用。
        message['callbackId'] = callbackId;
    }
    //把消息放入消息列表
    sendMessageQueue.push(message);
    //下面這句話會(huì)出發(fā)JS對(duì)OC的調(diào)用
    //讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
    //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

其實(shí)剩下的過程就很類似了估盘,就不多說了

參考文章

https://juejin.im/entry/6844903472718938126

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓷患,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子遣妥,更是在濱河造成了極大的恐慌擅编,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箫踩,死亡現(xiàn)場離奇詭異爱态,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)境钟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門锦担,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慨削,你說我怎么就攤上這事洞渔。” “怎么了理盆?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵痘煤,是天一觀的道長。 經(jīng)常有香客問我猿规,道長衷快,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任姨俩,我火速辦了婚禮蘸拔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘环葵。我一直安慰自己调窍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布张遭。 她就那樣靜靜地躺著邓萨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪菊卷。 梳的紋絲不亂的頭發(fā)上缔恳,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音洁闰,去河邊找鬼歉甚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛扑眉,可吹牛的內(nèi)容都是我干的纸泄。 我是一名探鬼主播赖钞,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼聘裁!你這毒婦竟也來了雪营?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤咧虎,失蹤者是張志新(化名)和其女友劉穎卓缰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砰诵,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年捌显,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茁彭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扶歪,死狀恐怖理肺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情善镰,我是刑警寧澤妹萨,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站炫欺,受9級(jí)特大地震影響乎完,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜品洛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一树姨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桥状,春花似錦帽揪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至士飒,卻和暖如春查邢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背变汪。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工侠坎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裙盾。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓实胸,卻偏偏與公主長得像他嫡,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子庐完,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345