关于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
网友评论