公司开始让做一个新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
- (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对应的配置
```
li {padding:5}
button { width:700; height:100;margin:10}
```
五,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] ];(自己封装的方法,更新三个属性)
}
}
}
}
}];
```
六,捕获
//在发送请求之前,决定是否跳转 如果不实现这个代理方法,默认会屏蔽掉打电话等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];
}];
```
当然代码有些冗余,有待优化。不过有种拨开乌云见明月的感觉。。顿时感觉压力小了很多。。。