WWDC ​2017-WKWebView 新功能

前言

WKWebView 是苹果在iOS 8中,引入的新一代 webView 组件,用以替代 UIKit 中略显笨重,效率低下的 UIWebView。

WKWebView的优势是显而易见的。根据实测,使用了和Safari相同的Nitro JS Engine之后,性能提升了3~4倍;内存占用只有之前的三分之一左右。同时,因为控件暴露了更多的方法,开发者可以更灵活地基于WKWebView,使用混合开发模式,搭建自己的APP。

经过这三年的发展,确实有一批开发者慢慢将APP中用到的UIWebView逐步替换成了WKWebView,但也踩了无数的坑。因此,每一年WWDC,苹果都会对WKWebView中存在的问题进行修复,并且增加一些开发者呼声最高的新功能,今年WWDC 2017也不例外,苹果针对WKWebView,增添了以下三大功能

WKHTTPCookieStore

WKWebView中的cookie管理是被开发者吐槽最多的领域之一。为了提升页面性能,WKWebView会在独立的进程中发起网络请求,因此,对于不同WKWebView间、WKWebView和主进程NSHTTPCookieStorage间的cookie同步,经常会出现无法共享或者写入延迟等问题。 第一个问题比较流行的解决方案是,在多个WKWebView之间共享同一个WKProcessPool来解决。第二个问题会更棘手一些,需要利用替换系统NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法来解决这个问题。或者稍微麻烦一点,比如首次请求将cookie放入请求header,然后通过页面的document.cookie等方法来获取。

本次苹果为WKWebView新增了一个simple的cookie管理功能,用户可以方便的为某一个WKWebView,增删cookie。方法很简单:

    WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
   //get cookies
    [cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
        NSLog(@"All cookies %@",cookies);
    }];

    //set cookie
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSHTTPCookieName] = @"userid";
    dict[NSHTTPCookieValue] = @"123";
    dict[NSHTTPCookieDomain] = @"koubei.com";
    dict[NSHTTPCookiePath] = @"/";

    NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
    [cookieStroe setCookie:cookie completionHandler:^{
        NSLog(@"set cookie");
    }];

    //delete cookie
    [cookieStroe deleteCookie:cookie completionHandler:^{
        NSLog(@"delete cookie");
    }];

除了增删取(支持http-only的cookie),开发还能通过KVO的方式获取cookie的变更。说句老实话,苹果并没有给出WKWebView之前cookie共享问题的一个解决方案,只是方便了开发者用别的方式绕过而已。开发者如果不用特殊的方式,依然需要本地自己对cookie做缓存,然后在新的容器打开的的时候手动做缓存共享。因此在WWDC的session上,演讲者介绍这块功能时,鼓掌并不踊跃,可见大家都觉得苹果诚意不足。。

WKContentRuleList

有这样一个需求:某些教育、商用APP需要能够过滤Web中的某些内容。因此,苹果在iOS 9中引入了Safari Content-Blocking的功能,用户可以通过一个extension,对Safari 和 Safari View Controller 做一些内容过虑,如屏蔽cookie,将http转换为https等。iOS 11中,这块功能也来到了WKWebView。

这块功能本质上就是开发者通过某些规则,写出一个JSON对象,然后将其插入某个WKWebView,然后WKWebView就会根据这些规则做内容过滤。由于WKWebView会将规则编译成bytecode,所以不用担心性能上的过多损失。

规则需要首先被编译到WKContentRuleListStore之中。

NSArray *jsonObj = @[@{
        @"trigger": @{
            @"url-filter": @".*" },
        @"action": @{
            @"type": @"make-https"
        } }];

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObj options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonStr = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];

    //编译规则
    [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"ContentBlockingRules"
                                                        encodedContentRuleList:jsonStr
                                                             completionHandler:^(WKContentRuleList *ruleList, NSError *err) {
                                                                 NSLog(@"ruleList %@",ruleList);
                                                             }];

通过Identifier可以寻找之前编译的规则,然后注入容器之中。

[[WKContentRuleListStore defaultStore] lookUpContentRuleListForIdentifier:@"ContentBlockingRules"
                                                            completionHandler:^(WKContentRuleList *ruleList, NSError *error) {
                                                                if (ruleList) {
                                                                    [self.webView.configuration.userContentController addContentRuleList:ruleList];
                                                                }
                                                            }];

这块功能感觉用的很少,可以作为一个技术点储备着。这篇文章详细介绍了这个新功能,可以参考。

WKURLSchemeHandler

对于混合开发模式来说,native和H5环境的互相调用是一个必不可少的功能。之前的WKWebView版本中,JS需要调用Native方法,可以通过如下方式:

//OC注册供JS调用的方法
[[_webView configuration].userContentController addScriptMessageHandler:self name:@"closeMe"];

//OC在JS调用方法做的处理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSLog(@"JS 调用了 %@ 方法,传回参数 %@",message.name,message.body);
}

//JS调用
    window.webkit.messageHandlers.closeMe.postMessage(null);

这样显得有点繁琐,而且JS环境的平台兼容性也不够好。于是,苹果这次推出了一个新功能,其实就是一个可以自定义的scheme解析器,用户可以处理前端抛出的类似“alipaym://xxx”这样的请求。

使用方法也很简单,注册一个delegate给容器,然后实现处理scheme的方法即可。

- (void)_schemeHandler
{
    [self.webView.configuration setURLSchemeHandler:self forURLScheme:@"alipaym"];
}

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
{
    //随便返回个data,可以根据需求自定义
    NSData *data = [urlSchemeTask.request.URL.host dataUsingEncoding:(NSUTF8StringEncoding)];
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
                                                        MIMEType:@"text/html"
                                           expectedContentLength:data.length
                                                textEncodingName:nil];

    [urlSchemeTask didReceiveData:data];
    [urlSchemeTask didReceiveResponse:response];
    [urlSchemeTask didFinish];
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
{
    NSLog(@"host %@",urlSchemeTask.request.URL.host);
}

苹果这个功能,怎么说呢,确实是一个相当有用的东西,但感觉它依然存在诸多限制,比如scheme有保留关键字,只支持scheme不够灵活等。更重要的是,JS调用Native的功能,本来就是所有JSBridge框架必有的功能,同时,框架会自己定义一套交互协议。也就是说,WKURLSchemeHandler提供的新功能对于开发者来说,都是已经实现了的东西,可能苹果提供了一个更简单的默认方法,但对于开发者来说,迁移的动力就不大了。

后记

WKWebView发展多年,业界可以说对他是又爱又恨,苹果似乎感觉在这方面投入比想象中的也要小,每年的更新有些时候让人觉得不痛不痒。也许过段时间回头来看,对其的认识会更加深刻吧。

发布于 2017-07-16