美文网首页IOS
WKWebView使用及踩坑

WKWebView使用及踩坑

作者: 好有魔力 | 来源:发表于2018-12-13 17:23 被阅读23次

    关于WKWebView

    从iOS9.0开始,苹果推荐App在访问web内容时使用WKWebView.相比于UIWebView, WKWebView具有加载速度更快,占用内存更小等优势.
    但是由于WKWebView把原来 UIWebView的代理拆解成了13个类和代理,在使用上也更加复杂.
    在使用中也有许多坑需要踩,我将从以下几个方面来细说都有哪些坑.
    1.基本使用
    2.WKWebView与js交互
    3.WKWebView H5 微信支付
    4.WKWebView 缓存策略

    基本使用

    WKPreferences *preferences = [WKPreferences new]; preferences.javaScriptEnabled = YES;
    preferences.minimumFontSize = 10.0;
    //此处要记得打开 不然WKWebView 不能响应前端 window.open()方法
    preferences.javaScriptCanOpenWindowsAutomatically = YES;
    configuration.preferences = preferences;
    
    WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height - 0) configuration:configuration];
        
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    webView.allowsBackForwardNavigationGestures = YES;
        
    NSURLRequest *request = /*your request*/
    [webView loadRequest:request];
        
    //添加KVO 检测完成进度 可以用来实现进度条
    [self.wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew
                            context:NULL];
                            
        //添加KVO title 检测标题变化           
    [self.wkWebView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew
                            context:NULL];
        
    

    不同于UIWebView, WKWebView 的有两个代理,顾名思义 UIDelegate 和 navigationDelegate,其中UIDelegate用来响应由WEB 发起的一些和UI有关的事件,UINavigationDelegate 则处理由网页跳转相应的事件.

    WKUIDelegate

    官方文档比较坑, 就一句话 Creates a new web view. /(ㄒoㄒ)/~~ ,看完了完全不知道是干什么用的,其实这个方法就是在前端调用window.open() 方法时调用的,需要创建一个新的WKWebView返回

    - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
         if (navigationAction.request.URL) {
            WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:webView.frame configuration:configuration];
            wkWebView.UIDelegate = self;
            wkWebView.navigationDelegate = self;
            [webView loadRequest:navigationAction.request];
            return wkWebView;
        }
        return nil;
    }
    
    

    此方法响应js alert() 方法, alert 只是弹出一个具有确定功能的对话框
    ,可以在这个方法中自定义弹出View的样式

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            completionHandler();
        }];
        [alert addAction:confirmAction];
        [self presentViewController:alert animated:YES completion:nil];
        
    }
    

    此方法响应js confirm() 方法, confirm 只是弹出一个具有确定和取消功能的对话框,注意点击取消后, 后续的js方法是不会调用的

    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            completionHandler(YES);
        }];
        UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            completionHandler(NO);
        }];
        [alert addAction:confirmAction];
        [alert addAction:cancel];
        [self presentViewController:alert animated:YES completion:nil];
    }
    

    此方法响应js prompt() 方法, prompt() 只是弹出一个具有输入功能还有确定和取消功能的弹框.

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
     
       UIAlertController *alert = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert];
       __block UITextField *txf;
       [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
           txf = textField;
       }];
       UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
           completionHandler(txf.text);
       }];
       UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
           completionHandler(@"取消");
       }];
       [alert addAction:confirmAction];
       [alert addAction:cancel];
       [self presentViewController:alert animated:YES completion:nil];
       
    }
    

    WKNavigationDelegate

    //在将要跳转网址时决定此次跳转是都被允许
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
        //根据条件务必调用一次 decisionHandler(WKNavigationActionPolicyCancel) 或者decisionHandler(WKNavigationActionPolicyAllow)
    
    }
    
    //在开始跳转网址时被调用
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
       
    }
    
    //在开始跳转网页后,收到服务器的重定向事件
    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
        
    }
    
    //在得到网址响应后,决定是否取消本次网址跳转(Navigation)
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    //根据条件务必调用一次 decisionHandler(WKNavigationActionPolicyCancel) 或者decisionHandler(WKNavigationActionPolicyAllow)
    }
    
    
    //当WKWebView 开始接受网页内容时调用
    - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
        
    }
    
    //网址导航完成
    - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
     
    }
    
    //当网址导航出现错误时调用
    - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
        
    }
    
    //当WKWebView 内容处理过程结束时调用
    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0)) {
        
    }
    
    

    WKWebView与js交互

    js调用iOS方法

    //js 调用的 原生方法名为 jsCallOCWithPara  参数为 para
    window.webkit.messageHandlers.jsCallOCWithPara.postMessage(para);
    

    OC处理js方法调用

     //添加处理js调用OC方法的代理
     [wkwebview.configuration.userContentController addScriptMessageHandler:yourDelegateIntance name:@"jsCallOCWithPara"];
     
     //实现WKScriptMessageHandler 协议
     - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
     if ([message.name isEqualToString:@"jsCallOCWithPara"]) {
            id body = message.body;
            NSLog(@"jsCallOCWithPara:%@",body);
        }
    }
    
     //在不需要时移除代理对象
     //注意_wkWebView会强引用 这个代理对象,如果不调用这个方法移除代理会造成内存泄漏
     [_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"jsCallOCWithPara"];
     
    

    OC调用js方法

     //oc 给js 传递 json
     NSDictionary *dic = @{@"name":@"cxh",@"sex":@"man",@"bag":@[@"pencil",@"iphone Xs Max"]};    
     NSData *dicData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; 
      NSString *dicString = [[NSString alloc] initWithData:dicData encoding:NSUTF8StringEncoding];
       [webView evaluateJavaScript:[NSString stringWithFormat:@"ocCalljsWithJson(%@)",dicString] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
            NSLog(@"WKWebView 调用js回调:%@ , error:%@",response,error.description);
       }];
    

    WKWebView H5 微信支付

    WKWebView H5 微信支付不同于手机浏览器,手机浏览器在链接跳转时能自动打开手机上的微信App.这里就是坑,但是这只是一个小坑,还有大坑后面再讲,遇到坑苦思无果,只能上网百度,看一篇文章能解决问题是开心不过了.
    饮水思源, 这也是我写这篇博客的原因,希望我的这篇博客能帮助对WKWebView 有疑惑的iOS小伙伴,在这里也感谢那些曾经对我有帮助的文章的作者,我的参考链接已经放到了文章底部.
    先说第一个坑,APP内WKWebView不能直接打开微信,所以就需要我们搞点事,先上代码

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        
        static  NSString *wxWebPayScheme = @"微信注册的一级域名://";
        static  NSString *redirect_url = nil;
        
        NSString* reqUrl = navigationAction.request.URL.absoluteString;
    
        if ([reqUrl hasPrefix:@"weixin://"]) {
            [[UIApplication sharedApplication]openURL:navigationAction.request.URL];
            //bSucc是否成功调起微信
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        
        NSString *wxPre = @"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb";
        if ([navigationAction.request.URL.absoluteString hasPrefix:wxPre]) {
            
            BOOL installedWeiXin = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"weixin://"]];
            if (!installedWeiXin)  {
                decisionHandler(WKNavigationActionPolicyCancel);
               //未安装微信处理
                return;
            }
            
            NSString *refer = [navigationAction.request valueForHTTPHeaderField:@"Referer"];
            
            //是自己的Referer 说明参数已经修改完成
            if ([refer isEqualToString:wxWebPayScheme ]) {
                decisionHandler(WKNavigationActionPolicyAllow);
                return;
            }
            
            BOOL needChanged = NO;
            NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithString:navigationAction.request.URL.absoluteString];
            NSMutableArray *urlComponentsArr = urlComponents.queryItems.mutableCopy;
            for (int i = 0 ; i <urlComponentsArr.count ; i++) {
                NSURLQueryItem *item = urlComponentsArr[i];
                if ([item.name isEqualToString:@"redirect_url"]) {
                    redirect_url = item.value;
                    [urlComponentsArr removeObjectAtIndex:i];
                    needChanged = YES;
                    break;
                }
            }
            
            if (needChanged) {
                //修改redirect_url 对应的值
                NSURLQueryItem *newQuert = [[NSURLQueryItem alloc] initWithName:@"redirect_url" value:wxWebPayScheme];
                [urlComponentsArr addObject:newQuert];
                
                urlComponents.queryItems = urlComponentsArr;
                
                NSURL *finalUrl = [NSURL URLWithString:urlComponents.string];
                if (finalUrl) {
                    //给请求头添加Referer字段
                    NSMutableURLRequest *mRequest = [[NSMutableURLRequest alloc] initWithURL:finalUrl];
                    
                    [mRequest setValue:wxWebPayScheme forHTTPHeaderField:@"Referer"];
                    decisionHandler(WKNavigationActionPolicyCancel);
                    [webView loadRequest:mRequest];
                    return;
                }
                
            }
        }
        
        if ([reqUrl hasPrefix:wxWebPayScheme]) { //从微信返回
            decisionHandler(WKNavigationActionPolicyCancel);
            [webView goBack];
            NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:redirect_url]];
            [webView loadRequest:request];
            return;
        }
        
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    
    

    看了代码想必你已经知道了,关于第一个坑,H5微信支付包含一系列Url的跳 转,但是在要打开App时,链接schem 会变为weixin://.看到这个感觉是不是特别亲切?,所以用url拦截的方式检测到weixin:// 然后通过openUrl方法打开微信App.

    坑到这里并没有结束,当我支付完成或者取消之后直接跳到了safari,.并没有回到我app .当时我得心情是这样的,WTF?,但是仔细想想,微信要想返回我们的app,微信必须知道我们app URL Schemes,但是H5支付,微信如何知道我们App的URL Schemes? 在这里我也请教了一些人,有人说在info.plist 文件中设置URL Schemes 为app的 bundle id就可以.我也试了,但是不管用,不知道原因出在哪里?如果有知道的可以给我留言.

    对于不能回到自己App解决方案,还是通过url拦截的方式,拦截有https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb 前缀的url,并且修改query中的redirect_url 为 @"微信注册的一级域名://",然后在请求头中设置 Referer 为 @"微信注册的一级域名://". 这样微信就知道了我们App的URl schemes,就可以回到我们的App了.
    </br>记得从微信回到我们App之后,处理之前的redirect_url,这个参数对应的地址一般是支付接口,我们只要重新访问这个地址就可以了.关于 Referer 请求头和query中的 redirect_url 的作用,可以参考文档底部的参考链接

    WKWebView 缓存策略

    WKWebView 的缓存是自己管理的,不同于UIWebView.先来段代码感受下

    //清理WKWebView缓存
        NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
        NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
        [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
        }];
    

    我遇到的问题是,app嵌入的H5样式更新了,手机这边却没有更新.通过每次加载网页清除缓存,这个问题得到了解决.但是觉得这样做没有利用缓存所带来的性能优势.如何才能做到有更新就请求新的资源?现在我还不知道,有知道的也可以留言讨论.

    结语

    虽然完成了任务,但是还有许多地方不知道具体细节,

    "路漫漫其修远兮,吾将上下而求索"

    第一次写文章,有不对的地方欢迎大家指正.

    WKWebView微信支付参考链接

    https://www.jianshu.com/p/157b8ae457ef
    https://www.jianshu.com/p/c1973aacc774

    相关文章

      网友评论

        本文标题:WKWebView使用及踩坑

        本文链接:https://www.haomeiwen.com/subject/etubhqtx.html