WKWebView使用及踩坑

关于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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容