• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

WKWebView以和问题解决

武飞扬头像
Bob大叔
帮助1

WKWebView是在AppleWWDC 2014iOS 8OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。但是由于之前还要适配iOS7,所以就没有使用。现在项目都适配iOS 8以上了,所以就开始使用WKWebView了,但是发现在使用的时候有好多坑

1.WKWebView的基本介绍和使用

1.1 创建 跟UIWebview一样
// 创建WKWebView
    WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 设置访问的URL
    NSURL *url = [NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665"];
    // 根据URL创建请求
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    // WKWebView加载请求
    [webView loadRequest:request];
    // 将WKWebView添加到视图
    [self.view addSubview:webView];

1.2 UIWebView和WKWebView的代理方法做一个对比
1.2.1.准备加载页面
UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
 WKNavigationDelegate: - webView:didStartProvisionalNavigation:

1.2.2.内容开始加载
UIWebViewDelegate: - webViewDidStartLoad:
 WKNavigationDelegate: - webView:didCommitNavigation:

1.2.3.页面加载完成
UIWebViewDelegate: - webViewDidFinishLoad:
 WKNavigationDelegate: - webView:didFinishNavigation:

1.2.4.页面加载失败
UIWebViewDelegate: - webView:didFailLoadWithError:
 WKNavigationDelegate: - webView:didFailNavigation:withError:
 WKNavigationDelegate: - webView:didFailProvisionalNavigation:withError:

可以看到很简单,和UIWebView并没有多少差别,然而性能提高很多,如果你只是简单的集成个Web页到App,这些已经够了。不过很多时候并没有那么简单,还需要处理各种东西,那么接着往后看。

2.WKWebView和JavaScript的交互

WKWebViewJavaScript的交互主要涉及到两个方面,

  • 一个是OC调用JavaScript ,
  • 另一个是 JavaScript 调用OC的方法,

WebKit框架中,有WKWebView可以替换UIKitUIWebViewAppKitWebView,而且提供了在两个平台可以一致使用的接口。WebKit框架使得开发者可以在原生App中使用Nitro来提高网页的性能和表现,Nitro就是SafariJavaScript引擎,WKWebView不支持JavaScriptCore的方式但提供message handler的方式为JavaScriptNative通信。

2.1. OC调用JavaScript

OC调用JavaScrippt是相对来说比较简单的,只需要在调用的地方添加下面一句代码即可:

//showAlert()是js里面的方法,这样就可以实现调用js方法
[self.webView evaluateJavaScript:@"showAlert('参数')" completionHandler:^(id item, NSError * _Nullable error) {
        // Block中处理是否通过了或者执行JS错误的代码
    }];

2.2. JavaScript 调用OC的方法,相对来说复杂一点

这地方需要两个配置,一个是OC代码的配置,另一个是JS代码的配置,下面先说一下OC代码的配置,细心的小伙伴可能已经发现了,创建WKWebView的时候,除了有- initWithFrame:方法外,还有一个高端的方法:- initWithFrame:configuration:方法。

2.2.1.配置 WKWebView
// 创建配置
      WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
      // 创建UserContentController(提供JavaScript向webView发送消息的方法)
      WKUserContentController* userContent = [[WKUserContentController alloc] init];
      // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
//NativeMethod 这个方法一会要与JS里面的方法写的一样
      [userContent addScriptMessageHandler:self name:@"NativeMethod"];
      // 将UserConttentController设置到配置文件
      config.userContentController = userContent;
      // 高端的自定义配置创建WKWebView
      WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
      // 设置访问的URL
      NSURL *url = [NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665?"];
      // 根据URL创建请求
      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      // WKWebView加载请求
      [webView loadRequest:request];
      // 将WKWebView添加到视图
      [self.view addSubview:webView];
学新通
2.2.2.实现协议方法

好了,现在万事俱备,只欠东风了。东风是什么呢,就是该在哪儿处理。可以看到WKScriptMessageHandler的协议里面只有一个方法,就是:

 - userContentController:didReceiveScriptMessage:

就是在这个代理方法里面操作:如果JavaScript执行已经写好的:window.webkit.messageHandlers.NativeMethod.postMessage("就是一个桂呀");这行代码,这个代理方法就会走,并且会有个WKScriptMessage的对象,这个WKScriptMessage对象有个name属性,拿到之后你会发现,就是我们注册的NativeMethod这个字符串,这时候你就可以手动调用Native的方法了。如果有多个方法需要调用的话怎么办,看到JavaScriptpostMessage()方法有一个参数了没有,可以根据这里的参数来区分调用原生App的哪个方法。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
          // 判断是否是调用原生的
          if ([@"NativeMethod" isEqualToString:message.name]) {
              // 判断message的内容,然后做相应的操作
              if ([@"close" isEqualToString:message.body]) {

              }
          }
      }

2.2.3.JavaScript的配置

JavaScript调用Native的方法就需要前端和Native的小伙伴们配合了,需要前端的小伙伴在JS的方法中调用:

window.webkit.messageHandlers.方法名.postMessage(参数)

注意:

参数没有时传 (null)这里是个坑点

  • 第一:实现以上代码的时候不要忘记实现** WKScriptMessageHandler**协议
  • 第二:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除(dealloc方法),否则会造成内存泄漏。
//页面进入时创建
 -(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:JS_goPageSelectClass];
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:JS_goClasscardList];
}

//页面消失是移除
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:JS_goPageSelectClass];
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:JS_goClasscardList];
}

js区分AndroidiOS的方法

           var u = navigator.userAgent;
            var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
            var isiOS = !!u.match(/\(i[^;] ;( U;)? CPU. Mac OS X/); //iOS终端

                if(isAndroid){
                    window.Android.alipayOrder();
                }
                if(isiOS){
                    window.webkit.messageHandlers.alipayOrder.postMessage(r);
                }

3.WKWebView 默认不弹出js的alert问题

WKWebview 默认是不弹出jsalert 要想可以弹出alert 需要手动的设置代理实现

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler

协议方法
具体的实现方法是,我们采用源生的UIAlertController 来实现弹出框,获取js里面的alert内容显示出来

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
        NSLog(@"取消按钮==%@",message);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
        NSLog(@"确定按钮==%@",message);
    }])];

    [self presentViewController:alertController animated:YES completion:nil];

}

4.WKWebView 默认是不能识别电话号码

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

具体实现:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    NSURL *URL = navigationAction.request.URL;
    NSLog(@"获取到=====%@",URL);
    NSString *scheme = [URL scheme];
    UIApplication *app = [UIApplication sharedApplication];
    // 打电话
    if ([scheme isEqualToString:@"tel"]) {
        if ([app canOpenURL:URL]) {
            [app openURL:URL];
            // 一定要加上这句,否则会打开新页面
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

学新通

注:对应html代码采用的是a标签

<a href="tel:16606668888">识别电话号码16606668888,进行拨打电话</a>

5.WKWebView 拦截js通过window.open() 打开的窗口

- (WKWebView )webView:(WKWebView )webView createWebViewWithConfiguration:(WKWebViewConfiguration )configuration forNavigationAction:(WKNavigationAction )navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
会拦截到window.open()事件.
只需要我们在在方法内进行处理
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}

6.WKWebView解决文字显示太小问题

在使用WKWebView的时候,常常会碰到显示内容比实际css设置的样式不能正常显示,内容普遍的偏小。其实导致这样问题的根源是少了HTML5的meta标签。解决的办法可以在iOS端添加以下的内容,当然也可以让后台添加完整的HTML5的格式。如果要在iOS端指定字体的大小也是可以的(不推荐在客户端设置字体大小)。

 NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";

 WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];

WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;

_myWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0,CGRectGetMaxY(headerView.frame) 10, M_S.width,M_S.height - CGRectGetMaxY(headerView.frame) - 40) configuration:wkWebConfig];

客户端设置字体大小eg:

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
    //修改字体大小 300%
    [ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '300%'" completionHandler:nil];

    //    //修改字体颜色  #6566b6
    //    [ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextFillColor= '#6566b6'" completionHandler:nil];

}

7.WKWebView 几个不常用的特性

iOS 中对 Web 的支持可以分为两个阶段:UIWebView 以及后来的 WKWebView。自 iOS 12 起,UIWebView 就开始被弃用。而在不久的将来,Apple 甚至不接受带有 UIWebView 的应用程序提交。WKWebViewWebKit框架的一部分,在应用程序的主线程之外运行,从而有助于其稳定性和卓越的性能。
首先,要加载内容,我们只需执行以下操作:

guard let url = URL(string: string) else { return }
letrequest = URLRequest(url: url)
webView?.load(request)

除了内容加载和 CSS 样式外,WKWebView 还可以做很多事情。
以下部分是WKWebView相对用得较少的一些功能清单。

7.1 .截获 Web URL

通过实现WKNavigationDelegate 协议的 definePolicyFor 函数,我们可以在导航期间截获URL。以下代码段显示了如何完成此操作:

funcwebView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let urlString = navigationAction.request.url?.absoluteString ?? ""
let pattern = "interceptSomeUrlPattern"
if urlString.contains(pattern){
var splitPath = urlString.components(separatedBy: pattern)
 }
}
7.2 JavaScript Alert

默认情况下,来自JavaScript 的提示不会显示在WKWebView中,因为它不是 UIKit 的一部分。因此,我们需要实现 WKUIDelegate 协议,以便显示提示信息中的警告、确认或文本输入等。
以下几种提示类型对应的方法:

funcwebView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void)

funcwebView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void)


funcwebView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {

let alertController = UIAlertController(title: nil, message: prompt, preferredStyle: .alert)

 alertController.addTextField { (textField) in
 textField.text = defaultText
 }
 alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action) in
iflet text = alertController.textFields?.first?.text {
 completionHandler(text)
 } else {
 completionHandler(defaultText)
 }
 }))

self.present(alertController, animated: true, completion: nil)
 }
学新通

学新通

7.3 配置URL操作

使用decisionPolicyFor 函数,您不仅可以通过电话,facetime 和邮件等操作来控制外部导航,还可以选择限制某些 URL 的打开。以下代码展示了每种情况:

funcwebView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

guardlet url = navigationAction.request.url else {
 decisionHandler(.allow)
return
 }

if ["tel", "sms", "mailto"].contains(url.scheme) && UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
 decisionHandler(.cancel)
 } else {
iflet host = navigationAction.request.url?.host {
if host == "www.wkwebview.com" {
 decisionHandler(.cancel)
 }
else{
 decisionHandler(.allow)
 }
 }
 }
 }
}
学新通
7.4 使用 WKWebView 进行身份验证

WKWebView 中的 URL 需要用户授权时,您需要实现以下方法:

funcwebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

let authenticationMethod = challenge.protectionSpace.authenticationMethod
if authenticationMethod == NSURLAuthenticationMethodDefault || authenticationMethod == NSURLAuthenticationMethodHTTPBasic || authenticationMethod == NSURLAuthenticationMethodHTTPDigest {
//Do you stuff
 }
 completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
}

收到身份验证质询后,您可以确定所需的身份验证类型(用户凭据或证书),并相应地使用提示或预定义凭据来处理条件。

7.5 多个 WKWebView 共享 Cookie

WKWebView 的每个实例都有其自己的cookie 存储。为了在 WKWebView的多个实例之间共享 cookie,我们需要使用 WKHTTPCookieStore,如下所示:

let cookies = HTTPCookieStorage.shared.cookies ?? []
for (cookie) in cookies {
 webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}
7.6 获取加载进度

WKWebView 的其他功能非常普遍,例如显示正在加载的 URL 的进度更新。
可以通过侦听以下方法的 estimatedProgresskeyPath 值来更新 ProgressViews:

overridefuncobserveValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

8. WKWebView Cookie存储

业界普遍认为WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的Cookie 容器 NSHTTPCookieStorage中。

实践发现WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie或服务器 set-cookie注入的 Cookie会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

比如,NSHTTPCookieStorage中存储了一个Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2021 23:38:25 GMT;

通过 WKWebView发起请求http://qq.com, 请求头不会自动带上cookie: Nicholas=test

8.2 解决办法

由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:

  • WKWebView loadRequest前,在 request header 中设置 Cookie, 解决首个请求 Cookie带不上的问题;
WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665?spm=1000.2115.3001.5343"]]; 

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];
  • 通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;

注意:document.cookie()无法跨域设置 cookie

WKUserContentController* userContentController = [WKUserContentController new]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 

[userContentController addUserScript:cookieScript];

这种方案无法解决302请求的 Cookie 问题,比如,第一个请求是 www.ba.com,我们通过在 request header 里带上 Cookie解决该请求的 Cookie 问题,接着页面302跳转到 www.bd.com,这个时候 www.bc.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:

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

可以在该回调函数里拦截302请求,copy request,在 equest header 中带上cookie并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame请求。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbjhjg
系列文章
更多 icon
同类精品
更多 icon
继续加载