WKWebView有14个类
WKBackForwardList:之前访问过的web页面的列表,可以通过后退和前进动作来访问到。
WKBackForwardListItem:webView中后退列表里的某一个网页。
WKFrameInfo:包含一个网页的布局信息
WKNavigation:包含一个网页的加载进度信息。
WKNavigationAction:包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。
WKNavigationResponse:包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。
WKPreferences:概括一个webiew的偏好设置。
WKProcessPool:表示一个web内容进程池。
WKUserContentController:提供使用JavaScript post信息和注册script的方法。
WKScriptMessage:包含网页发出的信息。
WKUserScript:表示可以被网页接受的用户脚本。
WKWebViewConfiguration:初始化webview的设置。
WKWindowFeatures:控制加载新网页时的窗口属性。
3个协议
WKNavigationDelegate:提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。
WKUIDelegate:提供用原生控件显示网页的方法回调
WKScripMessageHandler:提供从网页中收消息的回调方法。
所有相关的类的API
//上文介绍过的偏好配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// 用户交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 页面前进、后退列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;
// 默认构造器
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
//加载请求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 加载URL
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);
// 直接加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 直接加载data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
// 前进或者后退到某一页面
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;
// 页面的标题,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSString *title;
// 当前请求的URL,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
// 标识当前是否正在加载内容中,支持KVO的
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
// 当前加载的进度,范围为[0, 1]
@property (nonatomic, readonly) double estimatedProgress;
// 标识页面中的所有资源是否通过安全加密连接来加载,支持KVO的
@property (nonatomic, readonly) BOOL hasOnlySecureContent;
// 当前导航的证书链,支持KVO
@property (nonatomic, readonly, copy) NSArray *certificateChain NS_AVAILABLE(10_11, 9_0);
// 是否可以招待goback操作,它是支持KVO的
@property (nonatomic, readonly) BOOL canGoBack;
// 是否可以执行gofarward操作,支持KVO
@property (nonatomic, readonly) BOOL canGoForward;
// 返回上一页面,如果不能返回,则什么也不干
- (nullable WKNavigation *)goBack;
// 进入下一页面,如果不能前进,则什么也不干
- (nullable WKNavigation *)goForward;
// 重新载入页面
- (nullable WKNavigation *)reload;
// 重新从原始URL载入
- (nullable WKNavigation *)reloadFromOrigin;
// 停止加载数据
- (void)stopLoading;
// 执行JS代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
// 标识是否支持左、右swipe手势是否可以前进、后退
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
// 自定义user agent,如果没有则为nil
@property (nullable, nonatomic, copy) NSString *customUserAgent NS_AVAILABLE(10_11, 9_0);
// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0);
#if TARGET_OS_IPHONE
/*! @abstract The scroll view associated with the web view.
*/
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
#endif
#if !TARGET_OS_IPHONE
// 标识是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;
// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;
// 根据设置的缩放因子来缩放页面,并居中显示结果在指定的点
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;
使用
1.首先要引入WebKit库
#import <WebKit/WebKit.h>
2.两种初始化方法
// 默认初始化
- (instancetype)initWithFrame:(CGRect)frame;
// 根据对webview的相关配置,进行初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
3.加载网页
最基础的方法和WebView一样
NSURL *URL= [NSURL URLWithString:@"www.baidu,com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
一些其他的加载方法
//加载本地URL文件
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL;
//加载本地HTML字符串
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
//加载二进制数据
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSSTring *)MIMEType characterEncodingName:(NSString *)characterEndcodingName baseURL:(NSURL *)baseURL;
4.代理方法
1.【WKNavigationDelegate协议】
该代理提供的方法,可以用来跟踪加载过程(页面开始加载、加载完成、加载失败)、决定是否执行跳转。
// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailprovisionalNavigation:(WKNavigation *)navigation;
// 提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;
页面跳转的代理方法有三种,分为(收到挑转与决定是否跳转两种)
// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到相应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisonHandler;
// 在发送请求之前,决定是否跳转
- 需要响应身份验证时调用 同样在block中需要传入用户身份凭证
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
//用户身份信息
NSURLCredential * newCred = [[NSURLCredential alloc] initWithUser:@"user123" password:@"123" persistence:NSURLCredentialPersistenceNone];
//为 challenge 的发送方提供 credential
[challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
【WKUIDelegate协议】
WKUIDelegate从名称能看出它时webView在user interface 上的代理,共有5个可选类型的代理方法。它为webView提供了原生的弹框,而不是JavaScript里的提示框。虽然JavaScript的提示框可以做的跟原生一样,但是对于ios开发者来说,如果要能更改提示框就更方便了,提供这个代理,可以让ios端更加灵活的修改提示框的样式。
// 新建WKWebView
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
// 关闭WKWebView
- (void)webViewDidClose:(WKWebView *)webView NS_AVAILABLE(10_11, 9.0);
// 对应js的Alert方法
/**
* web界面中有弹出警告框时调用
*
* @param webView 实现该代理的webview
* @param message 警告框中的内容
* @param frame 主窗口
* @param completionHandler 警告框消失调用
*/
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
// 对应js的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
// 对应js的prompt方法(l例如输入框)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler;
【WKScriptMessageHandler】
这个协议中包含一个必须实现的方法,这个方法是native与web端交互的关键,它可以直接将接收到的JS脚本转为OC或Swift对象。
// 从web界面中接收到一个脚本时调用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
重点之WKWebView和JS交互
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>title</title>
<style>
*{
font-size: 50px;
}
.btn{height:80px; width:80%; padding: 0px 30px; background-color: #0071E7; border: solid 1px #0071E7; border-radius:5px; font-size: 1em; color: white}
</style>
<script>
function alertMobile() {
document.getElementById('mobile').innerHTML = '不带参数'
}
function alertName(msg) {
document.getElementById('name').innerHTML = '有一个参数 :' + msg
}
function alertSendMsg(num,msg) {
document.getElementById('msg').innerHTML = '有两个参数:' + num + ',' + msg + '!!'
}
function btnClick1() {
window.webkit.messageHandlers.showMobile.postMessage(null)
}
function btnClick2() {
window.webkit.messageHandlers.showName.postMessage('有一个参数')
}
function btnClick3() {
window.webkit.messageHandlers.showSendMsg.postMessage(['两个参数One', '两个参数Two'])
}
</script>
</head>
<body>
<br/>
<div>
<label>WKWebView&JS交互</label>
</div>
<br/>
<div id="mobile"></div>
<div>
<button class="btn" type="button" onclick="btnClick1()">不带参数</button>
</div>
<br/>
<div id="name"></div>
<div>
<button class="btn" type="button" onclick="btnClick2()">一个参数</button>
</div>
<br/>
<div id="msg"></div>
<div>
<button class="btn" type="button" onclick="btnClick3()">两个参数</button>
</div>
</body>
</html>
OC部分:
需要遵守的代理
1.设置偏好设置,以及JS调用OC添加处理脚本,这里的内容我写在了viewDidLoad里面,但是需要注意的是,需要在我们结束的时候释放WKUserController,不然会造成内存泄漏
- (void)viewDidLoad {
[super viewDidLoad];
// 设置偏好设置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 默认为0
config.preference.miniumFontSize = 10;
// 是否支持JavaScript
config.preferences.javaScriptEnabled = YES;
// 不通过用户交户,是否可以打开窗口
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height/2) configuration:config];
[self.view addSubview:self.webView];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
[self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePAth encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];
WKUserContentController *userCC = config.userContentController;
// JS 调用 OC 添加处理脚本
[userCC addScriptMessageHandler:self name:@"showMobile"];
[userCC addScriptMessageHandler:self name:@"showName"];
[userCC addScriptMessageHandler:self name:@"showSendMsg"];
}
释放WKUserContentController代码
- (void)removeAllScriptMsgHandle {
WKUserContentController *controller = self.web
}
在JS调用OC以后会走的代理
#pragma mark - WKScriptMessageHandler
// 直接将接收到的JS脚本转为OC或Swift对象。
- (void)userContentController:(WKUserContentController *)userController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"_cmd is %@", NSStringFromSelector(_cmd));
NSLog(@"%@", message.body);
NSDictionary *dict = message.body;
if (dict) {
NSString *functionName = [dict objectForKey:@"functionName"];
}
if ([message.name isEqualToString:@"showMobile"]) {
[self showMsg:@"没有参数"];
}
if ([message.name isEqualToString:@"showName"]) {
NSString *info = [NSString stringWithFormat:@"%@",message.body];
[self showMsg:info];
}
if ([message.name isEqualToString:@"showSendMsg"]) {
NSArray *array = message.body;
NSString *info = [NSString stringWithFormat:@"有两个参数: %@, %@ !!",array.firstObject,array.lastObject];
[self showMsg:info];
}
网页加载完成之后调用JS代码才会执行,因为这个时候html页面以及注入到webView中并且可以响应到对应的方法。OC调用JS代码
// 不带参数
- (IBAction)NOParameter:(id)sender {
[self.webView evaluateJavaScript:@"alertMobile()" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
// JS 返回结果
NSLog(@"%@ %@ ", response, error);
}];
}
// 一个参数
- (IBAction)oneParameter:(id)sender {
/*
*alertName('奥特曼打小怪兽')
*alertName JS方法名
*奥特曼打小怪兽 带的参数
*/
[self.webView evaluateJavaScript:@"alertName('奥特曼打小怪兽')" completionHandler:nil];
}
//两个参数
- (IBAction)twoParameter:(id)sender {
/*
*alertSendMsg('我是参数1', '我是参数2')
*alertSendMsg JS方法名
*我是参数1 第一个参数
*我是参数2 第二个参数
*/
[self.webView evaluateJavaScript:@"alertSendMsg('我是参数1', '我是参数2')" completionHandler:nil];
}
- (void)showMsg:(NSString *)msg {
[[[UIAlertView alloc] initWithTitle:nil message:msg delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
}
JS----->OC中URL拦截实现
// 针对一次action来决定是否允许跳转,允许与否都需要调用decisionHandler,比如decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKwebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisonHanler {
// 可以通过navigation.navigationType获取跳转类型,如新链接、后退等
NSURL *URL = navigationAction.request.URL.URL;
// 判断URL是否符合自定义的URL Scheme
if ([URL.scheme isEqualToString:SHWebViewDemoScheme]) {
// 根据不同的业务,来执行对应的操作,且获取参数
if ([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]) {
NSString *param = URL.query;
NSLog(@"短信验证码登录, 参数为%@", param);//短信验证码登录, 参数为username=12323123&code=892845
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHanler(WKNavigationActionPolicyAllow);
}
WKwebView相关的post请求实现
Html实现
<html>
<head>
<script>
function post(path, params) {
var method = "post";
var form = document.createElement("form");
form.setAttribute("method",method);
form.setAttribute("action",path);
for(var key in params) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type","hidden");
hiddenField.setAttribute("name",key);
hiddenField.setAttribute("value",params[key]);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</head>
<body>
</body>
</html>
OC中代码实现: 思路: 1 、将一个包含JavaScript的POST请求的HTML代码放到工程目录中 2 、加载这个包含JavaScript的POST请求的代码到WKWebView 3 、加载完成之后,用Native调用JavaScript的POST方法并传入参数来完成请求
- (void)viewDidLoad
{
self.needLoadJSPOST = YES;
[self loadHostPathURL:@"WKJSPOST"];
}
- (void)loadHostPathURL:(NSString *)url
{
NSString *path = [[NSBundle mainBundle] pathForResource:url ofType:@"html"];
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
if (self.needLoadJSPOST) {
[self postRequestWithJS];
self.needLoadJSPOST = NO;
}
}
#pragma mark - JSPOST
- (void)postRequestWithJS
{
NSString *jscript = [NSString stringWithFormat:@"post('%@',{%@})",self.URLString,self.postData];
NSLog(@"Javascript: %@", jscript);
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
NSLog(@"%@",error);
}];
}
Cookie相关
比起UIWebView 的自动管理,WKWebView 的Cookie 管理坑还是比较多的,注意事项如下: 1、WKWebView 加载网页得到的Cookie 会同步到NSHTTPCookieStorage 中 2、WKWebView加载请求时,不会同步NSHTTPCookieStorage 中已有的Cookie 3、通过共用一个WKProcessPool 并不能解决2中Cookie同步问题,且可能会造成Cookie丢失。
添加cookie
动态注入js
WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie = 'SyhCookie=Syh;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[UserContentController addUserScript:newCookieScript];
WKWebView执行js添加cookie.png
解决后续Ajax请求Cookie丢失问题
添加WKUserScript ,需保证sharedHTTPCookieStorage 中你的Cookie存在。
- (void)updateWebViewCookie
{
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[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]) {
if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
continue;
}
[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存在。
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
NSMutableURLRequest *fixedRequest;
if ([request isKindOfClass:[NSMutableURLRequest class]]) {
fixedRequest = (NSMutableURLRequest *)request;
} else {
fixedRequest = request.mutableCopy;
}
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 这里很重要
NSURLRequest *originalRequest = navigationAction.request;
[self fixRequest:originalRequest];
decisionHandler(WKNavigationActionPolicyAllow);
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)你就会发现,点击一个链接时,Request 的header 中多了Set-Cookie 字段,其实Cookie已经丢了。下面推荐笔者的解决方案,那就是把自己需要的Cookie 主动保存起来,每次调用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies 方法时,保证返回的数组中有自己需要的Cookie 。下面上代码,用了runtime 的Method Swizzling
首先是在适当的时候,保存
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<NSHTTPCookie *> *)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) {
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
Cookie 问题是目前 WKWebView 的一大短板
WKWebView Cookie存储
业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。
实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。
WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。
比如,NSHTTPCookieStorage 中存储了一个 Cookie:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
通过 UIWebView 发起请求http://y.qq.com, 则请求头会自动带上 cookie: Nicholas=test; 而通过 WKWebView发起请求http://y.qq.com, 请求头不会自动带上 cookie: Nicholas=test。
WKProcessPool
苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。
Workaround
由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:
a、WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;
WKWebView * webView = [WKWebView new];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
b、通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;
注意:document.cookie()无法跨域设置 cookie
WKUserContentController* userContentController = [WKUserContentController new];
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
这种方案无法解决302请求的 Cookie 问题,比如,第一个请求是 www.a.com,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 www.b.com,这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
可以在该回调函数里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。
WKWebView NSURLProtocol问题
WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了私有API:
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]
通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:
Class cls = NSClassFromString(@"WKBrowsingContextController”);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
}
但是这种方案目前存在两个严重缺陷:
a、post 请求 body 数据被清空
由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了
参考苹果源码:
https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 (复制链接到浏览器中打开)
及bug report:
https://bugs.webkit.org/show_bug.cgi?id=138169 (复制链接到浏览器中打开)
因此,如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空;
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];
}
// 开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
// 开始加载的时候,让加载进度条显示
self.progressView.hiddden = NO;
}
#pragma mark - j进度条
- (void)obserValueForkeyPath:(NSStrig *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)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:animate];
// Once complete,fade out UIProgressView
if (self.webView.estimatedProgress >= 1.0f) {
[UIView animateWithDuration:0.3f dealy:0.3f options:UIViewAnimationOptionCureEaseOut animations:^{
self.progressView.alpha = 0.0f;
} completion:^(BOOL finished) {
[self.progressView setProgress:0.0f animated:NO];
}];
}
}
else {
[super obserValueForKeyPath: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
{
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:^{}];
}
自定义contentInset刷新时页面跳动的bug
self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 49, 0);
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"];
WKWebView loadRequest 问题
在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:
//同样是由于进程间通信性能问题,HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];
workaround:
假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现:
- 替换请求 scheme,生成新的 post 请求 request2: post://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段);
- 通过-[WKWebView loadRequest:]加载新的 post 请求 request2;
- 通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;
- 注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index,将 request2 header的body 字段复制到 request3 的 body 中,并使用 NSURLConnection 加载 request3,最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;
WKWebView 页面样式问题
在 WKWebView 适配过程中,我们发现部分H5页面元素位置向下偏移或被拉伸变形,追踪后发现主要是H5页面高度值异常导致:
a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求,因此之前 webView 整个是从(0, 0)开始布局,通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height 的变化上,比如设置 webView.scrollView.contentInset.top = a ,那么contentSize.height 的值会增加a,导致H5页面长度增加,页面元素位置向下偏移;
解决方案是:调整WKWebView布局方式,避免调整webView.scrollView.contentInset 。实际上,即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset 的值,这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话,可以通过下面方式让H5页面恢复正常显示:
/**设置contentInset值后通过调整webView.frame让页面恢复正常显示
*参考:http://km.oa.com/articles/show/277372
*/
webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0);
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);
b. 在接入 now 直播的时候,我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况,最后发现是window.innerHeight 值不准确导致(在WKWebView上返回了一个非常大的值),而H5同学通过获取window.innerHeight 来设置页面高度,导致页面整体被拉伸。通过查阅相关资料发现,这个bug只在 iOS 9 的几个系统版本上出现,苹果后来fix了这个bug。我们最后的解决方案是:*延迟调用window.innerHeight*
setTimeout(function(){height = window.innerHeight},0);
or
Use shrink-to-fit meta-tag
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">
视频自动播放
WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction 设置是否允许自动播放,但一定要在 WKWebView 初始化之前设置,在 WKWebView 初始化之后设置无效。
goBack API问题
WKWebView 上调用 -[WKWebView goBack], 回退到上一个页面后不会触发window.onload() 函数、不会执行JS。
页面滚动速率
WKWebView 需要通过scrollView delegate 调整滚动速率:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}
一些实用的方法:
添加观察者
//添加 监测网页加载进度 的观察者
[self.webView addObserver:self
forKeyPath:@"estimatedProgress"
options:0
context:nil];
//添加 监测网页标题title 的观察者
[self.webView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
监听方法
//---kvo 监听进度 必须实现此方法
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
if ([keyPath isEqualToString:@"estimatedProgress"])
{
self.progressView.progress = self.webView.estimatedProgress;
if (self.progressView.progress == 1)
{
WeakSelfDeclare
[UIView animateWithDuration:0.25f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^
{
weakSelf.progressView.transform = CGAffineTransformMakeScale(1.0f, 1.4f);
}
completion:^(BOOL finished)
{
weakSelf.progressView.hidden = YES;
}];
}
}else if([keyPath isEqualToString:@"title"]
&& object == _webView){
self.navigationItem.title = _webView.title;
}else{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
移除观察者
[_webView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
[_webView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(title))];
清除WK缓存
- (void)cleanCacheAndCookie
{
//清除cookies
NSHTTPCookie *cookie;
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (cookie in [storage cookies])
{
[storage deleteCookie:cookie];
}
[[NSURLCache sharedURLCache] removeAllCachedResponses];
NSURLCache * cache = [NSURLCache sharedURLCache];
[cache removeAllCachedResponses];
[cache setDiskCapacity:0];
[cache setMemoryCapacity:0];
WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
[dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records)
{
for (WKWebsiteDataRecord *record in records)
{
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes
forDataRecords:@[record]
completionHandler:^
{
NSLog(@"Cookies for %@ deleted successfully",record.displayName);
}];
}
}];
}
- (void)dealloc
{
[_webView stopLoading];
[_webView setNavigationDelegate:nil];
[self clearCache];
[self cleanCacheAndCookie];
}
点击链接无反应
#pragma mark WKNavigationDelegate
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
WKWebView计算内容高度
添加KVO
[_webView.scrollView addObserver:selfforKeyPath:@"contentSize"options:NSKeyValueObservingOptionNewcontext:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentSize"]) {
dispatch_async(dispatch_get_global_queue(0,0), ^{
//document.documentElement.scrollHeight
//document.body.offsetHeight
[_webView evaluateJavaScript:@"document.documentElement.offsetHeight"completionHandler:^(id_Nullable result, NSError * _Nullable error) {
CGRect frame =_webView.frame;
frame.size.height = [result doubleValue] + 50;
_webView.frame = frame;
_scrollViewHeight =220 + _webView.height;
_scrollView.contentSize =CGSizeMake(kScreenWidth,_scrollViewHeight);
}];
});
}
}
H5调用了拨打电话功能
先拦截特点scheme,然后执行拨打电话的代码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 拦截
NSURL *URL = navigationAction.request.URL;
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"tel"]) {
NSString *resourceSpecifier = [URL resourceSpecifier];
NSString *callPhone = [NSString stringWithFormat:@"telprompt://%@", resourceSpecifier];
decisionHandler(WKNavigationActionPolicyCancel);
// 拨打
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:callPhone]];
return ;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
|