浅谈iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge源码分析

大家好,今天笔者分享一下自己在项目开发中,对iOS和H5之间的通信的一些知识点。主要是针对于苹果APP混合开发中的一点点小知识。然后是对WKWebViewJavascriptBridge,这个iOS原生OC代码和H5之间的通信桥接第三方包的一些分析。
首先在这里,先说明,这篇文章iOS这块的控件用的是WKWebview。因为再iOS12之后,UIWebview因为性能不足和内存占有过大等一系列问题,Apple公司最终决定将它慢慢移出iOS的舞台。
先来大致看下WKWebview一些常规用到的代理方法:

// --------------- WKNavigationDelegate -----------------
// 请求之间,决定是否跳转网页:一般用户在点击一个链接时跳转到下一个页面之间会调用这个方法
// 重点的代理方法,拦截webView 中url的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

// 获取响应数据之后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

// 开始加载页面时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

// 主机地址被重定向时调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

// 页面加载完毕时调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

// 跳转失败时调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

// 证书验证步骤:如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

//  ------- WKUIDelegate方面一个比较重要的代理 ---------------
// 在webview中使用alert函数时,需要执行这个代理,否则将无法打开
// 坏处在于,需要我们硬性执行代理方法,好处在于,可以在我们原生iOS这边自定义弹出框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler

接下来笔者说一下,最基础的iOS和js之间的通信方式,在WKWebview这边主要的方法:

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

这里我们和UIWebview中执行通信代码的方法比较一下:

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

可以看到,WKWebview这边的通信方法相对而言比较的人性化,因为iOS传递给js代码之后,得到的结果不一定是NSString类型,在很多情况下我们会需要得到一个对象、数组等等数据。
接下来,顺便提一下一个iOS和H5之间通信的架包:JavaScriptCore.framework。这个架包是苹果自带支持的架包,主要目的就是让开发人员更加简便的去操作iOS和H5之间的通信代码。导入方式是直接从build phases中导入。如下图:

浅谈iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge源码分析_第1张图片
image.png

接下来,我们正式进入,对WKWebViewJavascriptBridge这个第三方包的使用和源码的分析。首先,笔者这边给出了一个简单的使用WKWebViewJavascriptBridge的demo,代码地址: https://github.com/IBIgLiang/WKWebView-WebViewJavascriptBridge
。而WKWebViewJavascriptBridge这个第三方包的GitHub地址如下: https://github.com/marcuswestin/WebViewJavascriptBridge。里面已经很明确的指出的使用流程和注意事项。
这里笔者用一张图来总结一下这个过程,当然这些代码在demo中都有,大家可以照着图看下:
image.png

接下来,我们重点来看看这个第三方的源代码,我们这里通过流程图,大致看下整个过程。先说明,笔者这里暂时不考虑UIWebview的操作,因为笔者已经说了它将退出。作为iOS开发,我们就从iOS这边开始整个流程,先从ViewController开始:
浅谈iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge源码分析_第2张图片
image.png

从上图中可以看出,一般来说,VC中所作的基本上都是准备工作,无论是注册通信还是响应通信,除了当响应通信时,没有需要传递的参数, 因为WebViewJavascriptBridge本身所有的保存的参数的消息队列是统一处理的
接下来我们来看看,上面说过的一个重要的代理:

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

这个代理主要是做请求拦截操作,WebViewJavascriptBridge作者在拦截的过程中,加载WebViewJavascriptBridge_JS文件,然后是处理消息队列中的数据,具体我们还是一起看流程图:


浅谈iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge源码分析_第3张图片
image.png

这张图的发起点是在H5的JS端:

function setupWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            window.WVJBCallbacks = [callback];
            // 创建一个看不见的iframe,发起一个请求,这个请求用来加载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)
        }

调用setupWebViewJavascriptBridge这个方法之后,通过iFrame发送https://bridge_loaded请求,进入decidePolicyForNavigationAction代理中,然后走上面流程图中的流程。当地址是https://bridge_loaded时,此时直接进入[_base injectJavascriptFile];代码段。具体内容如下:

//TODO:WebViewJavascriptBridge桥接js文件的导入,并且发送iOS端业务数据给js
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    //导入桥接文件之后,将iOS端需要发送的数据发送给js端
    //self.startupMessageQueue中放置了iOS发起给js的数据
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            // 发送iOS端数据到JS端
            [self _dispatchMessage:queuedMessage];
        }
    }
}

主要做的就是导入js桥接问题,然后就是发送iOS端的业务参数,这个参数的来源已经在上面ViewController准备工作的流程图中写明。
除了JS端的iFrame发送https://bridge_loaded请求之外,还有WebViewJavascriptBridge_js中的https://wvjb_queue_message,这个请求发起的主要作用就是处理消息队列中的信息。也就是[self WKFlushMessageQueue];这个步骤。这个消息队列的处理包含了JS端发送的数据。代码如下:

//TODO:处理消息队列中的数据
- (void)WKFlushMessageQueue {
    
    // 读取js发送的数据
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        // 处理消息队列中的数据
        [_base flushMessageQueue:result];
    }];
}

[_base flushMessageQueue:result];这个方法中包含了JS端发送的数据和iOS处理完数据之后回调给JS端的数据的处理,包括两个部分:一个是iOS响应JS端通信后JS端回调给iOS端的消息内容,key值包含callbackId;另一个是JS响应iOS端的通信的消息内容,key值包含responseId。主要代码段:

NSString* responseId = message[@"responseId"];
        if (responseId) {
            // 如果是js注册函数中回调回来的数据,就直接回到iOS的callHandler的block中
            // js注册registerHandler -> iOS发送相应业务数据callHandler -> js中注册函数的block回调到iOS中
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            // iOS注册函数, js相应数据后,回到iOS注册函数registerHandler中的block,然后回调给js
            //iOS注册registerHandler -> js发送相应业务数据callHandler -> 进入iOS的registerHandler的block中 -> responseCallback回调给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发送给iOS之后,iOS处理完业务,重新将业务数据发送给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文件。这个js文件的内容其实很好理解,就是为iOS和JS两端的通信创建一个通用的WebViewJavascriptBridge对象:

// 定义一个webview和js之间的桥
    window.WebViewJavascriptBridge = {
        // 用于JS端注册通信
        registerHandler: registerHandler,
        // 用于存储JS端相应iOS端的通信的参数,放入_fetchQueue中
        callHandler: callHandler,
        // 请求超时字段
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        // 消息队列,用于放置JS端发送给iOS端的数据
        _fetchQueue: _fetchQueue,
        // 处理iOS端发送给JS端的数据
        _handleMessageFromObjC: _handleMessageFromObjC
    };

这个文件里面的方法中,需要重点说明一下的就是_dispatchMessageFromObjC这个方法中的这个代码段:

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

其实大家可以发现,这个代码段和笔者上面贴出来flushMessageQueue部分的那个代码段是相对应的,它就是JS端处理iOS发送过来的数据的过程。这里笔者就不具体展开了。
由于篇幅长度原因,关于iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge的源码分析就到此为止了。
笔者将在下一篇文章WKURLSchemeHandler在WKWebView的拦截请求中的使用中指出NSURLProtocol拦截这WKWebView无效的问题以及原因(scheme底层注册有关),并且使用WKURLSchemeHandler拦截请求,当前有个前提是这个请求的scheme是自定义的,有兴趣的同学可以瞄一眼!

你可能感兴趣的:(浅谈iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge源码分析)