初识WKWebView

公司开始让做一个新iOS项目,由于苹果的更新需要每次发版本审核,没法像服务器一样实时更新,技术部就讨论出原生+HTML混合编程的策略。一想到混合编程就毫不犹豫的开始用UIWebView,但是发现点击按钮十分卡顿,用户体验不是很好。想起苹果在2014年新出的WKWebView,想试下效果如何,结果让我很满意,心里甚是欢喜。但是.......

调用HTML必然牵扯到交互,以前在网上找的很好用的第三方框架(https://github.com/dukeland/EasyJSWebView)不适用WKWebView,刚开始还自以为是的想着把原来的UIWebView的代理方法替换成对应的WKWebView的代理方法就行了,就埋头在那里替换,替换完之后发现不行,就开始查看WKWebView的官方文档,还是我的功力太浅,看的一知半解,也不知道应该怎么去做,就只好去网上查找优秀的文章。(我想说的是,我解决问题的思路是有问题的,有点想当然的去做,或许应该先去了解WKWebView,对比着WKWebView和UIWebView的优劣,然后决定用哪个,而不是先做发现问题再去想办法解决,幸亏现在是把那些坑填满了。。。如果一开始的选择是个无底洞,不知道又要做多少无用功了)。以下内容就按照我遇到问题的顺序,对WKWebView做一总结:

一  、WKWebView简介

WKWebView是现代 WebKit API 在 iOS 8 和 OS X Yosemite 应用中的核心部分。它代替了 UIKit 中的UIWebView和 AppKit 中的WebView,提供了统一的跨双平台 API(iOS和OS)。

二、WKWebView新特性

在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存,模拟器加载百度与开源中国网站时,WKWebView占用23M,而UIWebView占用85M);

和 Safari 相同的 JavaScript 引擎,允许JavaScript的Nitro库加载并使用(UIWebView中限制);

支持了更多的HTML5特性;

自诩拥有 60fps 滚动刷新率、内置手势、高效的 app 和 web 信息交换通道

将UIWebViewDelegate与UIWebView重构成了14类与3个协议(具体查看苹果官方文档);

三、WKWebView不足


WKWebView会忽视默认的网络存储, NSURLCache, NSHTTPCookieStorage, NSCredentialStorage。WKWebView有自己的进程,同样也有自己的存储空间用来存储cookie和cache, 其他的网络类如NSURLConnection是无法访问到的。同时WKWebView发起的资源请求也是不经过NSURLProtocol的,导致无法自定义请求。同一个应用,不同WKWebView之间的cookie同步,可以通过使用同一个WKProcessPool实现cookie的同步。(我没有具体实现,如果想尝试,可以参考http://mcfeng.me/blog/2014/11/03/wkwebview-uiwebview-cookie-sync/)

简单来说就是页面之间cookie同步和共享,以前同一个应用,不同UIWebView之间的Cookie是自动同步的。它们都是保存在NSHTTPCookieStorage中。当UIWebView加载一个URL的时候,在加载完成时候,这个URL response中包含的cookie会自动以NSHttpCookie的形式保存到NSHTTPCookieStorage中。同时,如果在http response中,对cookie进行更新或者删除的话,其结果也会直接反应到NSHTTPCookieStorage存储的cookie数据中。所以,如果需要取出cookie数值,只要调用[NSHTTPCookieStorage sharedHTTPCookieStorage]即可。但是现在需要手动的设置cookie来保持页面之间信息同步。

四、WKWebView与HTML交互

WKWebView调用js脚本(以下为示例代码,作为参考)

```

#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{

NSString *executeString = @"$(document).ready(function(){$('.guanliansearch').removeClass(\"hide\").addClass(\"show\") });";

[self.webView evaluateJavaScript:executeString completionHandler:^(id _Nullable result, NSError * _Nullable error) {

}];

}

```

HTML调用OC方法

```
UIViewController<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>中WKWebView的初始化,注意要遵守协议

- (void)viewDidLoad {

[super viewDidLoad];

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

[configuration.userContentController addScriptMessageHandler:self name:@"goBack"];(goBack为方法名,与html中对应)

self.webView = [[WKWebView alloc] initWithFrame:viewRect configuration:configuration];

self.webView.navigationDelegate = self;

self.webView.UIDelegate = self;

self.webView.customUserAgent = kCustomUserAgent;


//让HTML适配屏幕

self.edgesForExtendedLayout = UIRectEdgeNone;

self.automaticallyAdjustsScrollViewInsets = NO;

[self.view addSubview:self.webView];

//显示加载页

[self showLoadingView];

// 1. URL 定位资源,需要资源的地址

NSURL *url = [NSURL URLWithString:self.webViewUrl];

// 2. 把URL告诉给服务器,请求,从m.baidu.com请求数据

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

// 设置缓存策略

request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

// 3. 发送请求给服务器

[self.webView loadRequest:request];

}```

```

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{

NSLog(@"%@",NSStringFromSelector(_cmd));

NSLog(@"message.body--%@  message.name--%@",message.body,message.name);

NSArray *messageArray = (NSArray *)message.body;(如果请求为多个参数,强转为数组)

if ([message.name isEqualToString:@"goBack"]){

[self  goBack];

}

}

- (void)goBack{

[self dismissViewControllerAnimated:YES completion:^{

}];

}```

HTML对应的配置

```

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<script type="text/javascript">//JS响应方法列表>

function btnClick1() {

var useragent = navigator.userAgent;

if(useragent == “***”){

window.webkit.messageHandlers.goBack.postMessage(null);(没有参数的话必须写null,如果不写按钮点击也没有反应,如果多个参数写法( ['param1','param2'])  ,即用数组格式;一个参数格式(‘param1’),goBack为方法名)

}

}</script>

<style type="text/css">body {background-color:white;font-size:20px;padding:10;}</style>

li {padding:5}

button { width:700; height:100;margin:10}

</head>

<body><ul><li><button type="button" onclick="btnClick1()">showToast></button></li></ul></body>

</html>

```

五,WKWebView  的WKUIDelegate方法的实现


```

#pragma mark - WKUIDelegate(我只是实现了2个作为参考,3个方法分别对应JavaScript 中创建三种消息框:警告框、确认框、提示框,如果不实现相应的方法,则HTML中写的消息框无反应)

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

WBLog(@"webView.URL---%@",webView.URL);

UIAlertController *alertController1 = [UIAlertController alertControllerWithTitle:message message: nil preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *noAction  = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

completionHandler();(该方法一定要实现,否则会系统会崩溃,错误日志很奇葩)

}];

[alertController1  addAction:noAction];

[self presentViewController:alertController1  animated:YES completion:^{

}];

}

- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{

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

UIAlertAction *cancelAction  = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

completionHandler(WKNavigationResponsePolicyCancel);

}];

[alertController  addAction:cancelAction];

[self presentViewController:alertController  animated:YES completion:^{

}];

UIAlertAction *okAction  = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

completionHandler(WKNavigationResponsePolicyAllow);

}];

[alertController  addAction:okAction];

}

```

五,WKWebView  的cookie共享


说道cookie肯定会想到session,首先简单了解下:(参考资料:http://blog.csdn.net/fangaoxin/article/details/6952954/)

会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份Session通过在服务器端记录信息确定用户身份

Cookie意为“甜饼”,是由W3C组织提出,最早由Netscape社区发展的一种机制。目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。

由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。

如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

以下是我手动设置cookie的代码,(希望有更好的方式,可以多多指教),思路是,当用户登录完成之后,保存页面返回的cookie,主要三个属性,JSESSIONID,uid,token
用户登录完成之后,每次访问页面都携带有这3个属性的cookie

```

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{

NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;

NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];

for (NSHTTPCookie *cookie in cookies) {

//每次都存最新的sessionId

[SettingBaseTool deleteDatasByKey:[cookie name]];

[SettingBaseTool saveBaseInfoStringWithKey:[cookie name] value:[cookie value] isDelayUserId:NO];

}

//跳转之前传入cookie

//js函数

NSString *JSFuncString =

@"function setCookie(name,value,expires)\

{\

var oDate=new Date();\

oDate.setDate(oDate.getDate()+expires);\

document.cookie=name+'='+value+';expires='+oDate;\

}\

function getCookie(name)\

{\

var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)'));\

if(arr != null) return unescape(arr[2]); return null;\

}\

function delCookie(name)\

{\

var exp = new Date();\

exp.setTime(exp.getTime() - 1);\

var cval=getCookie(name);\

if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString();\

}";

//拼凑js字符串

//取出 JSESSIONID  uid token

NSString *jessionString = @"JSESSIONID";

NSString *uidString = @"uid";

NSString *tokenString = @"token";

NSMutableString *JSCookieString = JSFuncString.mutableCopy;

NSString *jessionValueString = [SettingBaseTool readBaseInfoStringWithKey:jessionString isDelayUserId:NO];(从我自己封装的方法从沙盒中取出值)

if ([jessionValueString isNotEmpty]) {

NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", jessionString, jessionValueString];

[JSCookieString appendString:excuteJSString];

}

NSString *uidValueString = [SettingBaseTool readBaseInfoStringWithKey:uidString isDelayUserId:NO];

if ([uidValueString isNotEmpty]) {

NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", uidString, uidValueString];

[JSCookieString appendString:excuteJSString];

}

NSString  *tokenVauleString = [SettingBaseTool readBaseInfoStringWithKey:tokenString isDelayUserId:NO];

if ([tokenVauleString isNotEmpty]) {

NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", tokenString, tokenVauleString];

[JSCookieString appendString:excuteJSString];

}

//执行js

[webView evaluateJavaScript:JSCookieString completionHandler:^(id _Nullable result, NSError * _Nullable error) {

decisionHandler(WKNavigationResponsePolicyAllow);

}];

}


#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{

NSString *str = @"document.cookie";(获取页面返回的cookie,然后设置更新)

[webView evaluateJavaScript:str completionHandler:^(id _Nullable result, NSError * _Nullable error) {

if ([result isNotEmpty]) {

NSArray *resultArray  =[result componentsSeparatedByString:@";"];

if (resultArray.count > 0) {

for (NSString *resultString in resultArray) {

NSArray *keyValueArray = [resultString componentsSeparatedByString:@"="];

if (keyValueArray.count > 1) {

[SettingBaseTool updateDatasByKey:[keyValueArray[0] stringByReplacingOccurrencesOfString:@" " withString:@""] value:keyValueArray[1] ];(自己封装的方法,更新三个属性)

}

}

}

}

}];

```

六,捕获<a href:"9998989">

//在发送请求之前,决定是否跳转  如果不实现这个代理方法,默认会屏蔽掉打电话等url

```

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

// 允许跳转

decisionHandler(WKNavigationActionPolicyAllow);

}

//页面开始加载

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{

NSString *path=[webView.URL absoluteString];

NSString * newPath = [path lowercaseString];

//捕获URL,然后用UIApplication打开

if ([newPath hasPrefix:@"sms:"] || [newPath hasPrefix:@"tel:"] || [newPath hasPrefix:@"apipays:"] || [newPath hasPrefix:@"mqqwpa:"]) {

UIApplication * app = [UIApplication sharedApplication];

if ([app canOpenURL:[NSURL URLWithString:newPath]]) {

[app openURL:[NSURL URLWithString:newPath]];

}

return;

}

}

```

七,调用第三方,QQ,或者支付宝

点击QQ和支付宝都没有反应,思考原因,最后发现由于自定义了

self.webView.customUserAgent  = @“hah-ios”而第三方判断是浏览器还是Android或者iPhone的标准是基于默认值,所以就改成如下

```

[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable result, NSError * _Nullable error) {

NSString *defaultAgent = (NSString *)result;

self.webView.customUserAgent = [NSString stringWithFormat:@"%@ hah-ios",defaultAgent];

}];

```

当然代码有些冗余,有待优化。不过有种拨开乌云见明月的感觉。。顿时感觉压力小了很多。。。


推荐阅读更多精彩内容