41- WKWebView项目实践分享(六)- 项目实践:User Agent、跨域、重定向及其它

2字数 2043阅读 4453
强烈建议你把下边的参考文章也快速看下,作为拓展和补充。

设置User Agent

User Agent百度百科释义是。中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。
简单理解就是一句话:让服务器知道C端设备的信息,白话点就是, 你得让H5后台知道打开这个网页是从你们公司app上打开的,这对大数据和广告统计非常关键。

User Agent

设置方式两种,一种是iOS8.0开始都使用NSUserDefaults。另一种是从iOS9.0开始,使用WKWebView提供的API:customUserAgent。

 if ([UIDevice currentDevice].systemVersion.floatValue >= 9.0) 
{
NSString *newUserAgent = @"YCWebKit";
      self.wkWebView.customUserAgent = newUserAgent;
}

 if ([UIDevice currentDevice].systemVersion.floatValue < 9.0)
{
NSString *newUserAgent = @"YCWebKit";
      [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":newUserAgent}];
      [[NSUserDefaults standardUserDefaults] synchronize];   
}

注意:
1. NSUserDefaults这种方式一定要在初始化WKWebView之前设置才有效
**2. 后期项目使用中的过程中,遇到了一个关于User Agent的坑, 特别注意, 设置的时候不要覆盖手机原生User Agent, 我们要把我们自己公司的自定义User Agent字段追加到原生后边可以。否则会发生一些意想不到的错误。
具体看《42- WKWebView(6) - 补充: 实践中的坑》
**

跨域问题

跨域分成两种:

  1. 一个是在相同请求协议下,host不同。比如说,在http://www.a.com/点击一个按钮跳转到了http://www.b.com/页面是这个就叫做跨域。
  2. 直接请求协议就不同,这也是跨域。比如说:http://www.a.com/https://www.a.com/

跨域对WkWebView有什么影响呢?基于上一篇Cookie的方案,经过实践发现,在iOS11.0以下,WKWebView中HTTPS 对 HTTPS、HTTP 对 HTTP 的跨域是能载入的。但是没办法跨域用document.cookie设置cookie,也就是前一页面document.cookie中的cookie带不过去。 在iOS11.0以上,使用WKHttpCookieStore,从b.com页面执行goBack()方法返回到上一页a.com时,a.com的request Header中额外添加设置的appver和devised两个属性丢失,但是Cookie还在。

跨域问题的出现是因为WebKit框架对跨域进行了安全性检查限制,不允许跨域。那么怎么解决呢?我一共试验了两种解决方案。

第一种方案:修复

在请求过程中request是readOnly的,也就是我们没办法在请求过程中把丢失的属性在HTTPHeader中加上,继续请求。 所以只能是拦截到具体URL,然后重新赋值Cookie和其它参数,执行loadRequest。 但是这样在我们现两个问题行的导航条需求下,会出现两个问题:

  • webView.backForwardList.backList始终不会为空,导致如果点击返回退出控制器,需要
    手动加逻辑处理。
  • 当在a.com/下不跳转的情况下,对页面进行操作,界面变更之后。进入b.com/然后使用此
    方法重新loadRequest链接a.com/,页面恢复初始化,之前操作丢失。

第二种方案:新打开一个webView控制器

这种方案的顾虑如果a.com/要从b.com/页面中获取返回数据,会导致无法拿到数据。但是从公司H5开发小哥那里分享到经验,一般H5不会这么做,取数据只是在同一个页面中。那这样就很简单了。

和后台约定在http://b.com链接后边加上自定义标识,比如说OPenNewVC=1。那么此时我们在a.com/中点击某个按钮触发的跳转链接就是http://b.com?OPenNewVC=1,然后在decidePolicyForNavigationAction方法中拦截,然后打开新控制器。

设置重定向

在WKWebView中,网页如果有重定向的行为,会直接回调didReceiveServerRedirectForProvisionalNavigation。但是在实际测试中发现,有的网页虽然进入了这个方法,但是不需要我们手动干预,就可以重新跳转到重定向后的页面,你手动干预了反而导致请求不成功。但是有的网页,就需要自己重新loadRequest一下才可以。

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
    if (webView.isLoading
        && ![webView.URL.absoluteString containsString:@".a.com"]
        ) {
        [self loadRequestURL:webView.URL.absoluteString];
    }
}

白屏问题

在《腾讯Bugly: WKWebView 那些坑》的关于白屏问题的描述是这样的:

"WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,
Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊
讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process
的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Pr
ocess Memory + Other Process Memory)不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内
存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。"

总之,就是因为某种原因,Web Content Process奔溃了,从而出现白屏现象。

因为我的项目里,暂时没有遇到这个问题。所以大家可以先看一下腾讯的解决方案。

此处等待验证: 需要注意的一点是,我在之前的测试中,发现貌似上边提到的重定向失败也会进入这个方法。但是

处理a标签和_blank

需要通过navigationAction.targetFrame判断目标frame是不是主frame,如果不是主frame,那么就说明是新开一个tab操作。

- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
    WKFrameInfo *frameInfo = navigationAction.targetFrame;
    if(frameInfo == nil || frameInfo.isMainFrame == NO){
        [webView loadRequest:[YCWebViewCookieTool fixRequest:navigationAction.request]];
    }
    return nil;
}

处理Alert弹框

WKWebView把WebView调用native弹框的处理也交给我们,我们可以根据自己的需要进行定制。注意alertView点击之后需要调用一下代理方法中的block。

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }];
    [alert addAction:action1];
    [self presentViewController:alert animated:YES completion:NULL];
    
}


- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"删除" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }];
    
    UIAlertAction *action2 = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }];
    [alert addAction:action1];
    [alert addAction:action2];
    [self presentViewController:alert animated:YES completion:NULL];
}


- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:defaultText message:@"JS调用输入框" preferredStyle:UIAlertControllerStyleAlert];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.textColor = [UIColor redColor];
    }];
    
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler([[alert.textFields lastObject] text]);
    }]];
    
    [self presentViewController:alert animated:YES completion:NULL];
}

Https请求的证书验证

WKWebView中提供了didReceiveAuthenticationChallenge:方法来判断。我们可以弹个alert让用户选择是否信任,也可以默认直接设置信任。 以下的处理方式朋友分享的一个,源头可能来自《wkwebview下的https请求》:

/**
 https 请求会进这个方法,在里面进行https证书校验、白名单域名判断等操作
 */
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    
    /*
     NSURLSessionAuthChallengeUseCredential = 0,                     使用证书
     NSURLSessionAuthChallengePerformDefaultHandling = 1,            忽略证书(默认的处理方式)
     NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,     忽略书证, 并取消这次请求
     NSURLSessionAuthChallengeRejectProtectionSpace = 3,            拒绝当前这一次, 下一次再询问
     */
    
    NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];
    
    // 判断服务器返回的证书类型, 是否是服务器信任
    if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        
        SecTrustRef secTrustRef = challenge.protectionSpace.serverTrust;
        
        if (secTrustRef != NULL) {// 信任是否为空
            
            SecTrustResultType result;
            
            OSErr er = SecTrustEvaluate(secTrustRef, &result);
            
            if (er != noErr) {// 是否有错误信息
                
                completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace,nil);
                return;
                
            }else{// 没有错误信息
                
                if (result == kSecTrustResultRecoverableTrustFailure) {// 证书不受信任
                    CFArrayRef secTrustProperties = SecTrustCopyProperties(secTrustRef);

                    NSArray *arr = CFBridgingRelease(secTrustProperties);

                    NSMutableString *errorStr = [NSMutableString string];

                    for (int i=0;i<arr.count;i++){

                        NSDictionary *dic = [arr objectAtIndex:i];

                        if (i != 0 ) {
                            [errorStr appendString:@" "];

                        }

                        [errorStr appendString:(NSString*)dic[@"value"]];

                    }

                    SecCertificateRef certRef = SecTrustGetCertificateAtIndex(secTrustRef, 0);

                    CFStringRef cfCertSummaryRef = SecCertificateCopySubjectSummary(certRef);

                    NSString *certSummary = (NSString *)CFBridgingRelease(cfCertSummaryRef);

                    NSString *title = @"该服务器无法验证";

                    NSString *message = [NSString stringWithFormat:@" 是否通过来自%@标识为 %@证书为%@的验证. \n%@" , @"我的app",webView.URL.host,certSummary, errorStr];

                    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];

                    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"

                                                                        style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {


                                                                            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);


                                                                        }]];

                    [alertController addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {

                        NSURLCredential* credential = [NSURLCredential credentialForTrust:secTrustRef];

                        completionHandler(NSURLSessionAuthChallengeUseCredential, credential);

                    }]];

                    // 弹出权限提示框
                    [self presentViewController:alertController animated:YES completion:^{}];
                    return;
                    
                }else{// 证书受信任
                    
                    NSURLCredential* credential = [NSURLCredential credentialForTrust:secTrustRef];
                    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
                    return;
                }
            }
            
        }else{//信任不为空
            completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
        }
    }else{//非服务器信任
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

关于在线播放视频

需要注意mediaTypesRequiringUserActionForPlayback这个属性设置哪些媒体资源需要用户手动操作一下才能播放,也就是否自动播放。WKAudiovisualMediaTypeNone代表视频和音频资料都自动播放。

WKWebViewConfiguration * webConfiguration = [[WKWebViewConfiguration alloc]init];
WKUserContentController *contentController = [[WKUserContentController alloc] init];

// 是否允许HTML5页面在线播放视频,否则使用native播放器
webConfiguration.allowsInlineMediaPlayback = YES; 

// 是指不需要用户操作,进入webView页面视频自动播放
if (YCSystemVersionValue > 10.0) {
    webConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
}
else if (9.0 < YCSystemVersionValue && YCSystemVersionValue < 10.0) {
    webConfiguration.requiresUserActionForMediaPlayback = NO;
}
else if (8.0 < YCSystemVersionValue && YCSystemVersionValue< 9.0) {
    webConfiguration.mediaPlaybackRequiresUserAction = NO;
}

关于selectionGranularity属性

selectionGranularity这个属性是设置了用户拷贝网页内容的时候的粒度。粒度可能很不好理解。我们直接找个新闻网页看下设置之后的效果。当设置为WKSelectionGranularityCharacter, 在iOS9上复制文本没有定位光标。
具体可以看下:


iOS

API,from:《WKWebView详解》

屏幕旋转

// 屏幕旋转
wkWebView.translatesAutoresizingMaskIntoConstraints = NO;
wkWebView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);

NSURLProtocol

WKWebView中使用NSURLProtocol需要使用私有API,而且用了之后有两个问题。网上的一些方案可以过审,但是考虑到我们项目并非必要这个需求和使用之后的不确定性以及工作量。最终放弃NSURLProtocol。不过先期也了解了一下,提供大家几篇不错的文章参考:

参考

交流


希望能和大家交流技术
Blog:http://www.lilongcnc.cc


推荐阅读更多精彩内容