WKWebView 使用详情

引言:

最近公司的项目中大量使用了webview加载H5,鉴于WKWebView的性能优于UIWebView,所以就选择了WKWebViewWKWebView在使用的过程中,还是有很过内容值得我们去记录和研究的,这里我就做了一下总结,跟大家分享一下.


# 一. 优势

  • 更多的支持HTML5的特性
  • 官方宣称的高达60fps的滚动刷新率以及内置手势
  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。文档
  • Safari相同的JavaScript引擎
  • 占用更少的内存

# 二. 配置及创建

	/// 创建网页配置对象
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.allowsInlineMediaPlayback = YES;                     // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
    config.mediaTypesRequiringUserActionForPlayback = YES;      // 设置视频是否需要用户手动播放  设置为NO则会允许自动播放
    config.allowsPictureInPictureMediaPlayback = YES;           // 设置是否允许画中画技术 在特定设备上有效
    config.applicationNameForUserAgent = @"ChinaDailyForiPad";  // 设置请求的User-Agent信息中应用程序名称  ‼️iOS9后可用
    
    /// 创建设置对象
    WKPreferences *preference = [[WKPreferences alloc] init];
    preference.minimumFontSize = 0;                          // 最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
    preference.javaScriptEnabled = YES;                      // 设置是否支持javaScript 默认是支持的
    preference.javaScriptCanOpenWindowsAutomatically = YES;  // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
    config.preferences = preference;

    
    /// 这个类主要用来做native与JavaScript的交互管理
    WKUserContentController * wkUController = [[WKUserContentController alloc] init];
    [wkUController addScriptMessageHandler:self name:@"register"];     // js 执行 原生的 register 方法
    [wkUController addScriptMessageHandler:self name:@"goBack"];       // js 执行 原生的 goBack 方法
    config.userContentController = wkUController;
    
    /// 初始化
    _webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];
    
    _webView.UIDelegate = self;                         // UI代理
    _webView.navigationDelegate = self;                 // 导航代理
    _webView.allowsBackForwardNavigationGestures = YES; // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
   
    
    /// 加载本地 html
    NSString *path = [[NSBundle mainBundle] pathForResource:@"JStoOC.html" ofType:nil];
    NSString *htmlString = [[NSString alloc]initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [_webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]];
    
    /// 加载 URL
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    [_webView loadRequest:request];
    
    
    /// 一些方法
    WKBackForwardList *backForwardList = [_webView backForwardList]; // //可返回的页面列表, 存储已打开过的网页
    [_webView goBack];      // 页面后退
    [_webView goForward];   // 页面前进
    [_webView reload];      // 刷新当前页面
    [_webView reloadFromOrigin]; // 根据缓存有效期来刷新页面
    [_webView stopLoading]; // 停止加载页面


# 三. 代理方法

# 1. WKScriptMessageHandler

#pragma mark - ------ WKScriptMessageHandler ------
/// 通过接收JS传出消息的 name 进行捕捉的 回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    // js 执行原生方法 --  接收 js 的 register
    if ([message.name isEqualToString:@"register"]) {
        
        // 做一些原生操作。。。。。。。
    }
    
}

# 2. WKNavigationDelegate

#pragma mark - ------ WKNavigationDelegate ------

/// 根据 WebView 即将跳转的 HTTP 请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
    // 允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
    
    // 不允许跳转
    // decisionHandler(WKNavigationActionPolicyCancel);
}

/// 根据 WebView 接收的服务器响应头以及response相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    
    // 允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    // 不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

/// 当页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"didStartProvisionalNavigation");
}

/// 当页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    NSLog(@"didFailProvisionalNavigation");
}

/// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"didCommitNavigation");
}

/// 当页面加载完成后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    NSLog(@"didFinishNavigation");
}


/// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
    
}

/// 提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    NSLog(@"didFailNavigation");
}

/// 权限认证
-(void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {

    // https 证书相关
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([challenge previousFailureCount] == 0) {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    } else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

/// 进程被终止时调用 9.0才有
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
    
}

# 3. WKUIDelegate

#pragma mark - ------ WKUIDelegate ------
/// 页面是弹出窗口 _blank 处理
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

// 9.0 以后
- (void)webViewDidClose:(WKWebView *)webView  {
    
}


/// web界面中有 弹出警告框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                   message:message
                                                            preferredStyle:(UIAlertControllerStyleAlert)];
    
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"好的"
                                                     style:(UIAlertActionStyleCancel)
                                                   handler:^(UIAlertAction * _Nonnull action) {
                                                       completionHandler();
                                                   }];
    [alert addAction:action];
    [self presentViewController:alert animated:YES completion:nil];
    
}

/// web界面中有 弹出确认框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
                                                                   message:message
                                                            preferredStyle:(UIAlertControllerStyleAlert)];
    
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定"
                                                     style:(UIAlertActionStyleDefault)
                                                   handler:^(UIAlertAction * _Nonnull action) {
                                                       completionHandler(YES);
                                                   }];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消"
                                                           style:(UIAlertActionStyleCancel)
                                                         handler:^(UIAlertAction * _Nonnull action) {
                                                             completionHandler(NO);
                                                         }];
    
    [alert addAction:action];
    [alert addAction:cancelAction];
    
    [self presentViewController:alert animated:YES completion:nil];
}

/// web界面中有 弹出输入框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}

/// 是否显示预览  10.0才有
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {
    return YES;
}

/// 允许给指定的元素提供一个特殊的视图控制器  10.0才有
- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray<id <WKPreviewActionItem>> *)previewActions {
    
    return nil;
}

/// 长按弹出视图控制器时调用  10.0 才有
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {
    
}

# 四. 原生与 JS 交互

# 1. 注入 js 代码

    // 注入点击方法
    NSString *clickCode = @"function clickTo(t) { var dataId = t.getAttribute('data-id'); if (window.webkit) { window.webkit.messageHandlers.goToRecommend.postMessage(dataId); } else { window.AndroidWebView.goToRecommend(dataId) }};";
    [self.webView evaluateJavaScript:clickCode completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"=== 注入点击推荐列表 : %@",  (error != nil) ? @"error" : @"succ");
    }];

# 2. 执行 js 方法

    // 点击某个列表
    NSString *js = [NSString stringWithFormat:@"appRecoList(%@,%@)", uid, [DataManager manager].recommendJSONStr];
    [self.webView evaluateJavaScript:js completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"=== 点击某个列表 : %@",  (error != nil) ? @"error" : @"succ");
    }]; 

# 五. JS 与 原生 交互

# 1. js 代码中加入要执行的原生方法

	function closeRecTo() {

        if (window.webkit) { // 执行 iOS
            window.webkit.messageHandlers.closeRec.postMessage(null);
        } else { // 执行 安卓
            window.AndroidWebView.closeRec()
        }
    };

closeRec: 就是要执行的原生方法, 原生代码中要 监听 closeRec。

# 2. 添加交互监听

    /// 这个类主要用来做native与JavaScript的交互管理
    WKUserContentController * wkUController = [[WKUserContentController alloc] init];
    [wkUController addScriptMessageHandler:self name:@"register"];     // 注入 register 协议
    [wkUController addScriptMessageHandler:self name:@"goBack"];       // 注入 goBack 协议
    [wkUController removeScriptMessageHandlerForName:@"register"];     // 移除注入的协议  dealloc
    [wkUController removeAllUserScripts];                              // 移除所有注入的协议 dealloc
    config.userContentController = wkUController;

# 3. 代理方法接收监听

/// 通过接收JS传出消息的 name 进行捕捉的 回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    // js 执行原生方法 --  接收 js 的 closeRec
    if ([message.name isEqualToString:@"closeRec"]) {
        
        // 做一些操作。。。。。。。
    }
    
}

# 六. 网页内容加载进度条

# 1. 添加观察者

	//添加监测网页加载进度的观察者
    [self.webView addObserver:self
                   forKeyPath:@"estimatedProgress"
                      options:0
                      context:nil];
    //添加监测网页标题title的观察者
    [self.webView addObserver:self
                   forKeyPath:@"title"
                      options:NSKeyValueObservingOptionNew
                      context:nil];

# 2. kvo 监听进度

/// kvo 监听进度 必须实现此方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == _webView) {
        // 进度条
        NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
        self.progressView.progress = _webView.estimatedProgress;
        if (_webView.estimatedProgress >= 1.0f) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progressView.progress = 0;
            });
        }
        
    } else if ([keyPath isEqualToString:@"title"] && object == _webView ) {
        // 标题
        self.navigationItem.title = _webView.title;
        
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

# 3. 移除观察者

//移除观察者
[_webView removeObserver:self
              forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];

[_webView removeObserver:self
              forKeyPath:NSStringFromSelector(@selector(title))];

# 七. 常见问题

# 1. url中文处理

- (NSURL *)url{
	#pragma clang diagnostic push
	#pragma clang diagnostic ignored"-Wdeprecated-declarations"
    return [NSURL URLWithString:(NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, (CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]", NULL,kCFStringEncodingUTF8))];
#pragma clang diagnostic pop
}

# 2. 添加userAgent信息

有时候H5的伙伴需要我们为webview的请求添加userAgent,以用来识别操作系统等信息,但是如果每次用到webview都要添加一次的话会比较麻烦,下面介绍一个一劳永逸的方法。

Appdelegate中添加一个WKWebview的属性,启动app时直接,为该属性添加userAgent

- (void)setUserAgent {
    _webView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [_webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
        if (error) { return; }
        NSString *userAgent = result;
        if (![userAgent containsString:@"/mobile-iOS"]) {
            userAgent = [userAgent stringByAppendingString:@"/mobile-iOS"];
            NSDictionary *dict = @{@"UserAgent": userAgent};
            [TKUserDefaults registerDefaults:dict];
        }
    }];
}

这样一来,在app中创建的webview都会存在我们添加的userAgent的信息。

# 3. 请求Cookie

Cookie是 WKWebView 的一大短板。

需要在loadRequest前手动来添加Cookie,在 request header中设置 Cookie, 解决首个请求 Cookie带不上的问题:

- (void)injectCookies:(NSMutableURLRequest *)request{

    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    NSString *validDomain = request.URL.host;
    if (!cookies || cookies.count < 1) {
        return;
    }

    NSMutableString *cookieString = [NSMutableString stringWithString:@""];
    for (NSHTTPCookie *cookie in cookies) {
        if (![validDomain hasSuffix:cookie.domain]) {
            continue;
        }
        [cookieString appendString:[NSString stringWithFormat:@"%@=%@;", cookie.name, cookie.value]];
    }
    //删除最后一个“;”
    if (cookieString.length > 0) {
        [cookieString deleteCharactersInRange:NSMakeRange(cookieString.length - 1, 1)];
    }

    [request setValue:cookieString forHTTPHeaderField:@"Cookie"];
}

通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题

- (void)addUserCookieScript:(NSURLRequest *)request{
    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];

    if (!cookies || cookies.count < 1) {
        return;
    }
    NSMutableString *cookieScript = [NSMutableString stringWithString:@""];
    for (NSHTTPCookie *cookie in cookies) {
        [cookieScript appendString:[NSString stringWithFormat:@"document.cookie='%@';", [self javascriptStringWithCookie:cookie]]];
    }

    WKUserScript *script = [[WKUserScript alloc]initWithSource:cookieScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

    WKWebView *wkWebView = (WKWebView*)self.realWebView;
    [wkWebView.configuration.userContentController addUserScript:script];
}

- (NSString *)javascriptStringWithCookie:(NSHTTPCookie*)cookie {
    NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@;",
                        cookie.name,
                        cookie.value,
                        cookie.domain,
                        cookie.path ?: @"/"];

    if (cookie.secure) {
        string = [string stringByAppendingString:@"secure=true"];
    }
    return string;
}

# 4. 多个WKWebView之间共享Cookie

WKProcessPool这个类用来配置进程池,与网页视图的资源共享有关。WKProcessPool类中没有暴露任何属性和方法,所以拿不到任何数据。 为 多个WKWebView 配置为同一个WKProcessPool,会让多个 WKWebView 之间共享数据,例如Cookie、用户凭证等。

WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
configuration.processPool = [[WKWebViewPoolHandler sharedInstance] defaultPool];

# 5. 清除缓存

WebKit框架采用其本身的缓存框架,iOS 9 之后可以用WKWebsiteDataStore 类来清除缓存。

	//WKWebsiteDataTypeDiskCache,
    //WKWebsiteDataTypeOfflineWebApplicationCache,
    //WKWebsiteDataTypeMemoryCache,
    //WKWebsiteDataTypeLocalStorage,
    //WKWebsiteDataTypeIndexedDBDatabases,
    //WKWebsiteDataTypeWebSQLDatabases
    NSSet *websiteDataTypes = [NSSet setWithArray:@[WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage]];
    NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{}];

# 6. 处理被禁止的跳转

打开ituns.apple.com、跳转到appStore,、拨打电话,、唤起邮箱等一系列操作 WKWebView 不会自动交给UIApplication 来处理,除此之外,js端通过window.open() 打开新的网页的动作也被禁掉了, 需要我们自己处理:

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

   NSURL *url = navigationAction.request.URL;
    
    if ([url.scheme isEqualToString:@"tel"]){ // 打电话
        if (IS_IOS_10Later) {
            [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
        }else {
            [[UIApplication sharedApplication] openURL:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    if ([url.scheme isEqualToString:@"itms-services"]) { // 第三方下载
        if (IS_IOS_10Later) {
            [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
        }else {
            [[UIApplication sharedApplication] openURL:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    if ([url.absoluteString containsString:@"ituns.apple.com"]) { // 跳转到 appStroe
        if (IS_IOS_10Later) {
            [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
        }else {
            [[UIApplication sharedApplication] openURL:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    decisionHandler(WKNavigationActionPolicyAllow);
}

# 7.禁止长按出现复制框

在 webView 长按文字会出现下图效果:

解决方案:

    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"=== 禁止用户选择 : %@",  (error != nil) ? @"error" : @"succ");
    }];
    
    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"=== 禁止长按弹出框 : %@",  (error != nil) ? @"error" : @"succ");
    }];