淺談iOS中的WKWebView和H5之間通信及WKWebViewJavascriptBridge源碼分析

大家好,今天筆者分享一下自己在項(xiàng)目開發(fā)中,對iOS和H5之間的通信的一些知識點(diǎn)。主要是針對于蘋果APP混合開發(fā)中的一點(diǎn)點(diǎn)小知識。然后是對WKWebViewJavascriptBridge,這個(gè)iOS原生OC代碼和H5之間的通信橋接第三方包的一些分析。
首先在這里,先說明,這篇文章iOS這塊的控件用的是WKWebview。因?yàn)樵賗OS12之后,UIWebview因?yàn)樾阅懿蛔愫蛢?nèi)存占有過大等一系列問題,Apple公司最終決定將它慢慢移出iOS的舞臺。
先來大致看下WKWebview一些常規(guī)用到的代理方法:

// --------------- WKNavigationDelegate -----------------
// 請求之間,決定是否跳轉(zhuǎn)網(wǎng)頁:一般用戶在點(diǎn)擊一個(gè)鏈接時(shí)跳轉(zhuǎn)到下一個(gè)頁面之間會調(diào)用這個(gè)方法
// 重點(diǎn)的代理方法,攔截webView 中url的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

// 獲取響應(yīng)數(shù)據(jù)之后,決定是否跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

// 開始加載頁面時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

// 主機(jī)地址被重定向時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

// 頁面加載失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

// 頁面加載完畢時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

// 跳轉(zhuǎn)失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

// 證書驗(yàn)證步驟:如果需要證書驗(yàn)證,與使用AFN進(jìn)行HTTPS證書驗(yàn)證是一樣的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

//  ------- WKUIDelegate方面一個(gè)比較重要的代理 ---------------
// 在webview中使用alert函數(shù)時(shí),需要執(zhí)行這個(gè)代理,否則將無法打開
// 壞處在于,需要我們硬性執(zhí)行代理方法,好處在于,可以在我們原生iOS這邊自定義彈出框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler

接下來筆者說一下,最基礎(chǔ)的iOS和js之間的通信方式,在WKWebview這邊主要的方法:

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

這里我們和UIWebview中執(zhí)行通信代碼的方法比較一下:

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

可以看到,WKWebview這邊的通信方法相對而言比較的人性化,因?yàn)閕OS傳遞給js代碼之后,得到的結(jié)果不一定是NSString類型,在很多情況下我們會需要得到一個(gè)對象、數(shù)組等等數(shù)據(jù)。
接下來,順便提一下一個(gè)iOS和H5之間通信的架包:JavaScriptCore.framework。這個(gè)架包是蘋果自帶支持的架包,主要目的就是讓開發(fā)人員更加簡便的去操作iOS和H5之間的通信代碼。導(dǎo)入方式是直接從build phases中導(dǎo)入。如下圖:

image.png

接下來,我們正式進(jìn)入,對WKWebViewJavascriptBridge這個(gè)第三方包的使用和源碼的分析。首先,筆者這邊給出了一個(gè)簡單的使用WKWebViewJavascriptBridge的demo,代碼地址:https://github.com/IBIgLiang/WKWebView-WebViewJavascriptBridge
。而WKWebViewJavascriptBridge這個(gè)第三方包的GitHub地址如下:https://github.com/marcuswestin/WebViewJavascriptBridge。里面已經(jīng)很明確的指出的使用流程和注意事項(xiàng)。
這里筆者用一張圖來總結(jié)一下這個(gè)過程,當(dāng)然這些代碼在demo中都有,大家可以照著圖看下:
image.png

接下來,我們重點(diǎn)來看看這個(gè)第三方的源代碼,我們這里通過流程圖,大致看下整個(gè)過程。先說明,筆者這里暫時(shí)不考慮UIWebview的操作,因?yàn)楣P者已經(jīng)說了它將退出。作為iOS開發(fā),我們就從iOS這邊開始整個(gè)流程,先從ViewController開始:
image.png

從上圖中可以看出,一般來說,VC中所作的基本上都是準(zhǔn)備工作,無論是注冊通信還是響應(yīng)通信,除了當(dāng)響應(yīng)通信時(shí),沒有需要傳遞的參數(shù),因?yàn)閃ebViewJavascriptBridge本身所有的保存的參數(shù)的消息隊(duì)列是統(tǒng)一處理的。
接下來我們來看看,上面說過的一個(gè)重要的代理:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

這個(gè)代理主要是做請求攔截操作,WebViewJavascriptBridge作者在攔截的過程中,加載WebViewJavascriptBridge_JS文件,然后是處理消息隊(duì)列中的數(shù)據(jù),具體我們還是一起看流程圖:


image.png

這張圖的發(fā)起點(diǎn)是在H5的JS端:

function setupWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            window.WVJBCallbacks = [callback];
            // 創(chuàng)建一個(gè)看不見的iframe,發(fā)起一個(gè)請求,這個(gè)請求用來加載WebViewJavascriptBridge_JS.m中的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)
        }

調(diào)用setupWebViewJavascriptBridge這個(gè)方法之后,通過iFrame發(fā)送https://bridge_loaded請求,進(jìn)入decidePolicyForNavigationAction代理中,然后走上面流程圖中的流程。當(dāng)?shù)刂肥莌ttps://bridge_loaded時(shí),此時(shí)直接進(jìn)入[_base injectJavascriptFile];代碼段。具體內(nèi)容如下:

//TODO:WebViewJavascriptBridge橋接js文件的導(dǎo)入,并且發(fā)送iOS端業(yè)務(wù)數(shù)據(jù)給js
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    //導(dǎo)入橋接文件之后,將iOS端需要發(fā)送的數(shù)據(jù)發(fā)送給js端
    //self.startupMessageQueue中放置了iOS發(fā)起給js的數(shù)據(jù)
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            // 發(fā)送iOS端數(shù)據(jù)到JS端
            [self _dispatchMessage:queuedMessage];
        }
    }
}

主要做的就是導(dǎo)入js橋接問題,然后就是發(fā)送iOS端的業(yè)務(wù)參數(shù),這個(gè)參數(shù)的來源已經(jīng)在上面ViewController準(zhǔn)備工作的流程圖中寫明。
除了JS端的iFrame發(fā)送https://bridge_loaded請求之外,還有WebViewJavascriptBridge_js中的https://wvjb_queue_message,這個(gè)請求發(fā)起的主要作用就是處理消息隊(duì)列中的信息。也就是[self WKFlushMessageQueue];這個(gè)步驟。這個(gè)消息隊(duì)列的處理包含了JS端發(fā)送的數(shù)據(jù)。代碼如下:

//TODO:處理消息隊(duì)列中的數(shù)據(jù)
- (void)WKFlushMessageQueue {
    
    // 讀取js發(fā)送的數(shù)據(jù)
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        // 處理消息隊(duì)列中的數(shù)據(jù)
        [_base flushMessageQueue:result];
    }];
}

[_base flushMessageQueue:result];這個(gè)方法中包含了JS端發(fā)送的數(shù)據(jù)和iOS處理完數(shù)據(jù)之后回調(diào)給JS端的數(shù)據(jù)的處理,包括兩個(gè)部分:一個(gè)是iOS響應(yīng)JS端通信后JS端回調(diào)給iOS端的消息內(nèi)容,key值包含callbackId;另一個(gè)是JS響應(yīng)iOS端的通信的消息內(nèi)容,key值包含responseId。主要代碼段:

NSString* responseId = message[@"responseId"];
        if (responseId) {
            // 如果是js注冊函數(shù)中回調(diào)回來的數(shù)據(jù),就直接回到iOS的callHandler的block中
            // js注冊registerHandler -> iOS發(fā)送相應(yīng)業(yè)務(wù)數(shù)據(jù)callHandler -> js中注冊函數(shù)的block回調(diào)到iOS中
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            // iOS注冊函數(shù), js相應(yīng)數(shù)據(jù)后,回到iOS注冊函數(shù)registerHandler中的block,然后回調(diào)給js
            //iOS注冊registerHandler -> js發(fā)送相應(yīng)業(yè)務(wù)數(shù)據(jù)callHandler -> 進(jìn)入iOS的registerHandler的block中 -> responseCallback回調(diào)給js
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    // 將js發(fā)送給iOS之后,iOS處理完業(yè)務(wù),重新將業(yè)務(wù)數(shù)據(jù)發(fā)送給js端
                    [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);

到此,除了橋接js文件沒有涉及講解之外,其余重要步驟都已講解完畢。

============================================================

接下來就是最后的WebViewJavascriptBridge_JS文件。這個(gè)js文件的內(nèi)容其實(shí)很好理解,就是為iOS和JS兩端的通信創(chuàng)建一個(gè)通用的WebViewJavascriptBridge對象:

// 定義一個(gè)webview和js之間的橋
    window.WebViewJavascriptBridge = {
        // 用于JS端注冊通信
        registerHandler: registerHandler,
        // 用于存儲JS端相應(yīng)iOS端的通信的參數(shù),放入_fetchQueue中
        callHandler: callHandler,
        // 請求超時(shí)字段
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        // 消息隊(duì)列,用于放置JS端發(fā)送給iOS端的數(shù)據(jù)
        _fetchQueue: _fetchQueue,
        // 處理iOS端發(fā)送給JS端的數(shù)據(jù)
        _handleMessageFromObjC: _handleMessageFromObjC
    };

這個(gè)文件里面的方法中,需要重點(diǎn)說明一下的就是_dispatchMessageFromObjC這個(gè)方法中的這個(gè)代碼段:

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({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
        };
    }
    
    var handler = messageHandlers[message.handlerName];
    if (!handler) {
        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
    } else {
        handler(message.data, responseCallback);
    }
}

其實(shí)大家可以發(fā)現(xiàn),這個(gè)代碼段和筆者上面貼出來flushMessageQueue部分的那個(gè)代碼段是相對應(yīng)的,它就是JS端處理iOS發(fā)送過來的數(shù)據(jù)的過程。這里筆者就不具體展開了。
由于篇幅長度原因,關(guān)于iOS中的WKWebView和H5之間通信及WKWebViewJavascriptBridge的源碼分析就到此為止了。
筆者將在下一篇文章WKURLSchemeHandler在WKWebView的攔截請求中的使用中指出NSURLProtocol攔截這WKWebView無效的問題以及原因(scheme底層注冊有關(guān)),并且使用WKURLSchemeHandler攔截請求,當(dāng)前有個(gè)前提是這個(gè)請求的scheme是自定義的,有興趣的同學(xué)可以瞄一眼!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容