UIWebView & WKWebView 详解下

前言

一直想系统的总结下UIWebViewWKWebView,这里整理了一个
Demo可供参考

分为两部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下

简介

WKWebView是Apple于iOS 8.0推出的WebKit中的核心控件,用来替代UIWebViewWKWebViewUIWebView的优势在于:

  • 更多的支持HTML5的特性
  • 高达60fps的滚动刷新率以及内置手势
  • 与Safari相同的JavaScript引擎
  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)
  • 可以获取加载进度:estimatedProgress(UIWebView需要调用私有Api)

POST请求

WKWebView相关的post请求实现

Html实现


    
        
    
    
    


OC中代码实现:
思路:
1 、将一个包含JavaScript的POST请求的HTML代码放到工程目录中
2 、加载这个包含JavaScript的POST请求的代码到WKWebView
3 、加载完成之后,用Native调用JavaScript的POST方法并传入参数来完成请求

//仅当第一次的时候加载本地JS
// @property(nonatomic,assign) BOOL needLoadJSPOST;

- (void)viewDidLoad
{
     // JS发送POST的Flag,为真的时候会调用JS的POST方法
    self.needLoadJSPOST = YES;
    //POST使用预先加载本地JS方法的html实现,请确认WKJSPOST存在
    [self loadHostPathURL:@"WKJSPOST"];
}

- (void)loadHostPathURL:(NSString *)url
{
    //获取JS所在的路径
    NSString *path = [[NSBundle mainBundle] pathForResource:url ofType:@"html"];
    //获得html内容
    NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    //加载js
    [self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
}

//加载成功,对应UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView; 网页加载完成,导航的变化
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    /*
     主意:这个方法是当网页的内容全部显示(网页内的所有图片必须都正常显示)的时候调用(不是出现的时候就调用),,否则不显示,或则部分显示时这个方法就不调用。
     */
    // 判断是否需要加载(仅在第一次加载)
    if (self.needLoadJSPOST) {
        // 调用使用JS发送POST请求的方法
        [self postRequestWithJS];
        // 将Flag置为NO(后面就不需要加载了)
        self.needLoadJSPOST = NO;
    }
}

#pragma mark - JSPOST
// 调用JS发送POST请求
- (void)postRequestWithJS
{
    // 拼装成调用JavaScript的字符串
    NSString *jscript = [NSString stringWithFormat:@"post('%@',{%@})",self.URLString,self.postData];
    NSLog(@"Javascript: %@", jscript);
    //post('http://www.postexample.com',{"username":"aaa","password":"123"})
    // 调用JS代码
    [self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
        NSLog(@"%@",error);
    }];
}

UIWebView的post请求实现

- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL*url=[NSURL URLWithString:@"http://your_url.com"];
    NSString*body=[NSString stringWithFormat:@"arg1=%@&arg2=%@",@"val1",@"val2"];
    NSMutableURLRequest*request=[[NSMutableURLRequest alloc]initWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
    [self.webView loadRequest:request];
}

Cookie相关

WKWebView的cookie管理

比起UIWebView的自动管理,WKWebViewCookie管理坑还是比较多的,注意事项如下:
1、WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage
2、WKWebView加载请求时,不会同步NSHTTPCookieStorage中已有的Cookie
3、通过共用一个WKProcessPool并不能解决2中Cookie同步问题,且可能会造成Cookie丢失。

添加cookie

动态注入js

WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
 //添加自定义的cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie = 'SyhCookie=Syh;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加脚本
 [UserContentController addUserScript:newCookieScript];
UIWebView & WKWebView 详解下_第1张图片
WKWebView执行js添加cookie.png

解决后续Ajax请求Cookie丢失问题

添加WKUserScript,需保证sharedHTTPCookieStorage中你的Cookie存在。

/*!
 *  更新webView的cookie
 */
- (void)updateWebViewCookie
{
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    //添加Cookie
    [self.configuration.userContentController addUserScript:cookieScript];
}

- (NSString *)cookieString
{
    NSMutableString *script = [NSMutableString string];
    [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
        // Skip cookies that will break our script
        if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
            continue;
        }
        // Create a line that appends this cookie to the web view's document's cookies
        [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.da_javascriptString];
    }
    return script;
}

@interface NSHTTPCookie (Utils)

- (NSString *)da_javascriptString;

@end

@implementation NSHTTPCookie (Utils)

- (NSString *)da_javascriptString
{
    NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                        self.name,
                        self.value,
                        self.domain,
                        self.path ?: @"/"];
    if (self.secure) {
        string = [string stringByAppendingString:@";secure=true"];
    }
    return string;
}

@end

解决跳转新页面时Cookie带不过去问题

当你点击页面上的某个链接,跳转到新的页面,Cookie又丢了,需保证sharedHTTPCookieStorage中你的Cookie存在。

//核心方法:
/**
 修复打开链接Cookie丢失问题

 @param request 请求
 @return 一个fixedRequest
 */
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
    NSMutableURLRequest *fixedRequest;
    if ([request isKindOfClass:[NSMutableURLRequest class]]) {
        fixedRequest = (NSMutableURLRequest *)request;
    } else {
        fixedRequest = request.mutableCopy;
    }
    //防止Cookie丢失
    NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
    if (dict.count) {
        NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
        [mDict setValuesForKeysWithDictionary:dict];
        fixedRequest.allHTTPHeaderFields = mDict;
    }
    return fixedRequest;
}

#pragma mark - WKNavigationDelegate 

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

#warning important 这里很重要
    //解决Cookie丢失问题
    NSURLRequest *originalRequest = navigationAction.request;
    [self fixRequest:originalRequest];
    //如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳转
    //允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
    //可能有小伙伴,会说如果originalRequest是NSURLRequest,不可变,那不就添加不了Cookie了,是的,我们不能因为这个问题,不允许跳转,也不能在不允许跳转之后用loadRequest加载fixedRequest,否则会出现死循环,具体的,小伙伴们可以用本地的html测试下。
    
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

#pragma mark - WKUIDelegate

- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {

#warning important 这里也很重要
    //这里不打开新窗口
    [self.webView loadRequest:[self fixRequest:navigationAction.request]];
    return nil;
}

Cookie依然丢失

什么的方法需保证sharedHTTPCookieStorage中你的Cookie存在。怎么保证呢?由于WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage的特点,有时候你强行添加的Cookie会在同步过程中丢失。抓包(Mac推荐Charles)你就会发现,点击一个链接时,Requestheader中多了Set-Cookie字段,其实Cookie已经丢了。下面推荐笔者的解决方案,那就是把自己需要的Cookie主动保存起来,每次调用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies方法时,保证返回的数组中有自己需要的Cookie。下面上代码,用了runtimeMethod Swizzling

首先是在适当的时候,保存

//比如登录成功,保存Cookie
NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
for (NSHTTPCookie *cookie in allCookies) {
    if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
        NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
        if (dict) {
            NSHTTPCookie *localCookie = [NSHTTPCookie cookieWithProperties:dict];
            if (![cookie.value isEqual:localCookie.value]) {
                NSLog(@"本地Cookie有更新");
            }
        }
        [[NSUserDefaults standardUserDefaults] setObject:cookie.properties forKey:DAUserDefaultsCookieStorageKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        break;
    }
}

在读取时,如果没有则添加

@implementation NSHTTPCookieStorage (Utils)

+ (void)load
{
    class_methodSwizzling(self, @selector(cookies), @selector(da_cookies));
}

- (NSArray *)da_cookies
{
    NSArray *cookies = [self da_cookies];
    BOOL isExist = NO;
    for (NSHTTPCookie *cookie in cookies) {
        if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
            isExist = YES;
            break;
        }
    }
    if (!isExist) {
        //CookieStroage中添加
        NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
        if (dict) {
            NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
            [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
            NSMutableArray *mCookies = cookies.mutableCopy;
            [mCookies addObject:cookie];
            cookies = mCookies.copy;
        }
    }
    return cookies;
}

@end

UIWebView的cookie管理

UIWebViewCookie管理很简单,一般不需要我们手动操作Cookie,因为所有Cookie都会被[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例管理,而且UIWebView会自动同步CookieStorage中的Cookie,所以只要我们在Native端,正常登陆退出,h5在适当时候刷新,就可以正确的维持登录状态,不需要做多余的操作。

可能有一些情况下,我们需要在访问某个链接时,添加一个固定Cookie用来做区分,那么就可以通过header来实现
思路:
1、主动操作NSHTTPCookieStorage,添加一个自定义Cookie
2、读取所有Cookie
3、Cookie转换成HTTPHeaderFields,并添加到request的header


NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.URLString]];
//主动操作NSHTTPCookieStorage,添加一个自定义Cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
                                                            NSHTTPCookieName: @"customCookieName",
                                                            NSHTTPCookieValue: @"heiheihei",
                                                            NSHTTPCookieDomain: @".baidu.com",
                                                            NSHTTPCookiePath: @"/"

                                                            }];
//Cookie存在则覆盖,不存在添加
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
//读取所有Cookie
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];

UIWebView & WKWebView 详解下_第2张图片
UIWebView自定义cookie.png

自定义浏览器UserAgen

这个其实在App开发中,比较重要。比如常见的微信、支付宝App等,都有自己的UserAgent,而UA最常用来判断在哪个App内,一般App的下载页中只有一个按钮"点击下载",当用户点击该按钮时,在微信中则跳转到应用宝,否则跳转到AppStore。那么如何区分在哪个App中呢?就是js判断UA

//js中判断
if (navigator.userAgent.indexOf("MicroMessenger") !== -1) {
   //在微信中
}

关于自定义UA,这个UIWebView不提供Api,而WKWebView提供Api,前文中也说明过,就是调用customUserAgent属性。

self.webView.customUserAgent = @"WebViewDemo/1.1.0";    //自定义UA,只支持WKWebView

而有没有其他的方法实现自定义浏览器UserAgent呢?有

//最好在AppDelegate中就提前设置
@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    //设置自定义UserAgent
    [self setCustomUserAgent];
    return YES;
}

- (void)setCustomUserAgent
{
    //get the original user-agent of webview
    UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
    NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
    //add my info to the new agent
    NSString *newAgent = [oldAgent stringByAppendingFormat:@" %@", @"WebViewDemo/1.0.0"];
    //regist the new agent
    NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", newAgent, @"User-Agent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}

@end

说明:

1、通过NSUserDefaults设置自定义UserAgent,可以同时作用于UIWebView和WKWebView。
2、WKWebView的customUserAgent属性,优先级高于NSUserDefaults,当同时设置时,显示customUserAgent的值。

WKWebView自定义返回/关闭按钮

//返回按钮
@property (nonatomic)UIBarButtonItem* customBackBarItem;
//关闭按钮
@property (nonatomic)UIBarButtonItem* closeButtonItem;

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    [self updateNavigationItems];
    //允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    [self updateNavigationItems];
}


- (void)updateNavigationItems
{
    if (self.webView.canGoBack) {
        UIBarButtonItem *spaceButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
        spaceButtonItem.width = -6.5;
        [self.navigationItem setLeftBarButtonItems:@[spaceButtonItem,self.customBackBarItem,self.closeButtonItem] animated:NO];
    }else {
        self.navigationController.interactivePopGestureRecognizer.enabled = YES;
        [self.navigationItem setLeftBarButtonItems:@[self.customBackBarItem]];
    }
}

-(UIBarButtonItem*)customBackBarItem{
    if (!_customBackBarItem) {
        UIImage* backItemImage = [[UIImage imageNamed:@"backItemImage"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
        UIImage* backItemHlImage = [[UIImage imageNamed:@"backItemImage-hl"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
        
        UIButton* backButton = [[UIButton alloc] init];
        [backButton setTitle:@"返回" forState:UIControlStateNormal];
        [backButton setTitleColor:self.navigationController.navigationBar.tintColor forState:UIControlStateNormal];
        [backButton setTitleColor:[self.navigationController.navigationBar.tintColor colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted];
        [backButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
        [backButton setImage:backItemImage forState:UIControlStateNormal];
        [backButton setImage:backItemHlImage forState:UIControlStateHighlighted];
        [backButton sizeToFit];
        
        [backButton addTarget:self action:@selector(customBackItemClicked) forControlEvents:UIControlEventTouchUpInside];
        _customBackBarItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
    }
    return _customBackBarItem;
}

-(void)customBackItemClicked{
    if (self.webView.goBack) {
        [self.webView goBack];
    }else{
        [self.navigationController popViewControllerAnimated:YES];
    }
}

-(UIBarButtonItem*)closeButtonItem{
    if (!_closeButtonItem) {
        _closeButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"关闭" style:UIBarButtonItemStylePlain target:self action:@selector(closeItemClicked)];
    }
    return _closeButtonItem;
}

-(void)closeItemClicked{
    [self.navigationController popViewControllerAnimated:YES];
}

WKWebView添加进度条

- (void)viewDidLoad 
{
   //设置加载进度条
  //@property (nonatomic,strong) UIProgressView *progressView;
  //static void *WkwebBrowserContext = &WkwebBrowserContext;
  //添加进度条
   [self.view addSubview:self.progressView];
   [self.webView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:0 context:WkwebBrowserContext];
}

//开始加载,对应UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
    //开始加载的时候,让加载进度条显示
    self.progressView.hidden = NO;
}


#pragma mark - 进度条
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == self.webView) {
        self.progressView.alpha = 1.0f;
        BOOL animated = self.webView.estimatedProgress > self.progressView.progress;
        [self.progressView setProgress:self.webView.estimatedProgress animated:animated];
        // Once complete, fade out UIProgressView
        if (self.webView.estimatedProgress >= 1.0f) {
            [UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
                self.progressView.alpha = 0.0f;
            } completion:^(BOOL finished) {
                [self.progressView setProgress:0.0f animated:NO];
            }];
        }
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (UIProgressView *)progressView{
    if (!_progressView) {
        _progressView = [[UIProgressView alloc]initWithProgressViewStyle:UIProgressViewStyleDefault];
        if (_isNavHidden == YES) {
            _progressView.frame = CGRectMake(0, 20, self.view.bounds.size.width, 3);
        }else{
            _progressView.frame = CGRectMake(0, 64, self.view.bounds.size.width, 3);
        }
        // 设置进度条的色彩
        [_progressView setTrackTintColor:[UIColor colorWithRed:240.0/255 green:240.0/255 blue:240.0/255 alpha:1.0]];
        _progressView.progressTintColor = [UIColor greenColor];
    }
    return _progressView;
}

WKWebView填坑

js alert方法不弹窗

实现- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;方法,如果不实现,就什么都不发生,好吧,乖乖实现吧,实现了就能弹窗了。

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(nonnull void (^)(void))completionHandler
{
    //js 里面的alert实现,如果不实现,网页的alert函数无效
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }]];
    [self presentViewController:alertController animated:YES completion:^{}];
}

白屏问题

当WKWebView加载的网页占用内存过大时,会出现白屏现象。解决方案是

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
    [webView reload];   //刷新就好了
}

有时白屏,不会调用该方法,具体的解决方案是

比如,最近遇到在一个高内存消耗的H5页面上 present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是 webView.titile 会被置空, 因此,可以在 viewWillAppear 的时候检测 webView.title 是否为空来 reload 页面。(出自WKWebView 那些坑)

自定义contentInset刷新时页面跳动的bug
self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 49, 0);
//史诗级神坑,为何如此写呢?参考https://opensource.apple.com/source/WebKit2/WebKit2-7600.1.4.11.10/ChangeLog  
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]; //kvc给WKWebView的私有变量_obscuredInsets设置值

你可能感兴趣的:(UIWebView & WKWebView 详解下)