WKWebView 使用详情
引言:
最近公司的项目中大量使用了webview
加载H5,鉴于WKWebView
的性能优于UIWebView
,所以就选择了WKWebView
。WKWebView
在使用的过程中,还是有很过内容值得我们去记录和研究的,这里我就做了一下总结,跟大家分享一下.
# 一. 优势
- 更多的支持HTML5的特性
- 官方宣称的高达60fps的滚动刷新率以及内置手势
- 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。文档
- Safari相同的JavaScript引擎
- 占用更少的内存
# 二. 配置及创建
/// 创建网页配置对象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.allowsInlineMediaPlayback = YES; // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
config.mediaTypesRequiringUserActionForPlayback = YES; // 设置视频是否需要用户手动播放 设置为NO则会允许自动播放
config.allowsPictureInPictureMediaPlayback = YES; // 设置是否允许画中画技术 在特定设备上有效
config.applicationNameForUserAgent = @"ChinaDailyForiPad"; // 设置请求的User-Agent信息中应用程序名称 ‼️iOS9后可用
/// 创建设置对象
WKPreferences *preference = [[WKPreferences alloc] init];
preference.minimumFontSize = 0; // 最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
preference.javaScriptEnabled = YES; // 设置是否支持javaScript 默认是支持的
preference.javaScriptCanOpenWindowsAutomatically = YES; // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
config.preferences = preference;
/// 这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];
[wkUController addScriptMessageHandler:self name:@"register"]; // js 执行 原生的 register 方法
[wkUController addScriptMessageHandler:self name:@"goBack"]; // js 执行 原生的 goBack 方法
config.userContentController = wkUController;
/// 初始化
_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];
_webView.UIDelegate = self; // UI代理
_webView.navigationDelegate = self; // 导航代理
_webView.allowsBackForwardNavigationGestures = YES; // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
/// 加载本地 html
NSString *path = [[NSBundle mainBundle] pathForResource:@"JStoOC.html" ofType:nil];
NSString *htmlString = [[NSString alloc]initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[_webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]];
/// 加载 URL
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[_webView loadRequest:request];
/// 一些方法
WKBackForwardList *backForwardList = [_webView backForwardList]; // //可返回的页面列表, 存储已打开过的网页
[_webView goBack]; // 页面后退
[_webView goForward]; // 页面前进
[_webView reload]; // 刷新当前页面
[_webView reloadFromOrigin]; // 根据缓存有效期来刷新页面
[_webView stopLoading]; // 停止加载页面
# 三. 代理方法
# 1. WKScriptMessageHandler
#pragma mark - ------ WKScriptMessageHandler ------
/// 通过接收JS传出消息的 name 进行捕捉的 回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// js 执行原生方法 -- 接收 js 的 register
if ([message.name isEqualToString:@"register"]) {
// 做一些原生操作。。。。。。。
}
}
# 2. WKNavigationDelegate
#pragma mark - ------ WKNavigationDelegate ------
/// 根据 WebView 即将跳转的 HTTP 请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// 允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
// 不允许跳转
// decisionHandler(WKNavigationActionPolicyCancel);
}
/// 根据 WebView 接收的服务器响应头以及response相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
// 允许跳转
decisionHandler(WKNavigationResponsePolicyAllow);
// 不允许跳转
//decisionHandler(WKNavigationResponsePolicyCancel);
}
/// 当页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"didStartProvisionalNavigation");
}
/// 当页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"didFailProvisionalNavigation");
}
/// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
NSLog(@"didCommitNavigation");
}
/// 当页面加载完成后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"didFinishNavigation");
}
/// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
}
/// 提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"didFailNavigation");
}
/// 权限认证
-(void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
// https 证书相关
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([challenge previousFailureCount] == 0) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
/// 进程被终止时调用 9.0才有
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
}
# 3. WKUIDelegate
#pragma mark - ------ WKUIDelegate ------
/// 页面是弹出窗口 _blank 处理
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
// 9.0 以后
- (void)webViewDidClose:(WKWebView *)webView {
}
/// web界面中有 弹出警告框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
message:message
preferredStyle:(UIAlertControllerStyleAlert)];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"好的"
style:(UIAlertActionStyleCancel)
handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}];
[alert addAction:action];
[self presentViewController:alert animated:YES completion:nil];
}
/// web界面中有 弹出确认框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
message:message
preferredStyle:(UIAlertControllerStyleAlert)];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定"
style:(UIAlertActionStyleDefault)
handler:^(UIAlertAction * _Nonnull action) {
completionHandler(YES);
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消"
style:(UIAlertActionStyleCancel)
handler:^(UIAlertAction * _Nonnull action) {
completionHandler(NO);
}];
[alert addAction:action];
[alert addAction:cancelAction];
[self presentViewController:alert animated:YES completion:nil];
}
/// web界面中有 弹出输入框 时调用
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.text = defaultText;
}];
[alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(alertController.textFields[0].text?:@"");
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
/// 是否显示预览 10.0才有
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {
return YES;
}
/// 允许给指定的元素提供一个特殊的视图控制器 10.0才有
- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray<id <WKPreviewActionItem>> *)previewActions {
return nil;
}
/// 长按弹出视图控制器时调用 10.0 才有
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {
}
# 四. 原生与 JS 交互
# 1. 注入 js 代码
// 注入点击方法
NSString *clickCode = @"function clickTo(t) { var dataId = t.getAttribute('data-id'); if (window.webkit) { window.webkit.messageHandlers.goToRecommend.postMessage(dataId); } else { window.AndroidWebView.goToRecommend(dataId) }};";
[self.webView evaluateJavaScript:clickCode completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"=== 注入点击推荐列表 : %@", (error != nil) ? @"error" : @"succ");
}];
# 2. 执行 js 方法
// 点击某个列表
NSString *js = [NSString stringWithFormat:@"appRecoList(%@,%@)", uid, [DataManager manager].recommendJSONStr];
[self.webView evaluateJavaScript:js completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"=== 点击某个列表 : %@", (error != nil) ? @"error" : @"succ");
}];
# 五. JS 与 原生 交互
# 1. js 代码中加入要执行的原生方法
function closeRecTo() {
if (window.webkit) { // 执行 iOS
window.webkit.messageHandlers.closeRec.postMessage(null);
} else { // 执行 安卓
window.AndroidWebView.closeRec()
}
};
closeRec
: 就是要执行的原生方法, 原生代码中要 监听 closeRec。
# 2. 添加交互监听
/// 这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];
[wkUController addScriptMessageHandler:self name:@"register"]; // 注入 register 协议
[wkUController addScriptMessageHandler:self name:@"goBack"]; // 注入 goBack 协议
[wkUController removeScriptMessageHandlerForName:@"register"]; // 移除注入的协议 dealloc
[wkUController removeAllUserScripts]; // 移除所有注入的协议 dealloc
config.userContentController = wkUController;
# 3. 代理方法接收监听
/// 通过接收JS传出消息的 name 进行捕捉的 回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// js 执行原生方法 -- 接收 js 的 closeRec
if ([message.name isEqualToString:@"closeRec"]) {
// 做一些操作。。。。。。。
}
}
# 六. 网页内容加载进度条
# 1. 添加观察者
//添加监测网页加载进度的观察者
[self.webView addObserver:self
forKeyPath:@"estimatedProgress"
options:0
context:nil];
//添加监测网页标题title的观察者
[self.webView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
# 2. kvo 监听进度
/// kvo 监听进度 必须实现此方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == _webView) {
// 进度条
NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
self.progressView.progress = _webView.estimatedProgress;
if (_webView.estimatedProgress >= 1.0f) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.progressView.progress = 0;
});
}
} else if ([keyPath isEqualToString:@"title"] && object == _webView ) {
// 标题
self.navigationItem.title = _webView.title;
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
# 3. 移除观察者
//移除观察者
[_webView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
[_webView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(title))];
# 七. 常见问题
# 1. url中文处理
- (NSURL *)url{
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
return [NSURL URLWithString:(NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, (CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]", NULL,kCFStringEncodingUTF8))];
#pragma clang diagnostic pop
}
# 2. 添加userAgent信息
有时候H5的伙伴需要我们为webview
的请求添加userAgent
,以用来识别操作系统等信息,但是如果每次用到webview都要添加一次的话会比较麻烦,下面介绍一个一劳永逸的方法。
在Appdelegate
中添加一个WKWebview
的属性,启动app时直接,为该属性添加userAgent
:
- (void)setUserAgent {
_webView = [[WKWebView alloc] initWithFrame:CGRectZero];
[_webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
if (error) { return; }
NSString *userAgent = result;
if (![userAgent containsString:@"/mobile-iOS"]) {
userAgent = [userAgent stringByAppendingString:@"/mobile-iOS"];
NSDictionary *dict = @{@"UserAgent": userAgent};
[TKUserDefaults registerDefaults:dict];
}
}];
}
这样一来,在app中创建的webview
都会存在我们添加的userAgent的信息。
# 3. 请求Cookie
Cookie
是 WKWebView 的一大短板。
需要在loadRequest
前手动来添加Cookie
,在 request header
中设置 Cookie, 解决首个请求 Cookie
带不上的问题:
- (void)injectCookies:(NSMutableURLRequest *)request{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSString *validDomain = request.URL.host;
if (!cookies || cookies.count < 1) {
return;
}
NSMutableString *cookieString = [NSMutableString stringWithString:@""];
for (NSHTTPCookie *cookie in cookies) {
if (![validDomain hasSuffix:cookie.domain]) {
continue;
}
[cookieString appendString:[NSString stringWithFormat:@"%@=%@;", cookie.name, cookie.value]];
}
//删除最后一个“;”
if (cookieString.length > 0) {
[cookieString deleteCharactersInRange:NSMakeRange(cookieString.length - 1, 1)];
}
[request setValue:cookieString forHTTPHeaderField:@"Cookie"];
}
通过 document.cookie
设置 Cookie
解决后续页面(同域)Ajax、iframe
请求的 Cookie
问题
- (void)addUserCookieScript:(NSURLRequest *)request{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
if (!cookies || cookies.count < 1) {
return;
}
NSMutableString *cookieScript = [NSMutableString stringWithString:@""];
for (NSHTTPCookie *cookie in cookies) {
[cookieScript appendString:[NSString stringWithFormat:@"document.cookie='%@';", [self javascriptStringWithCookie:cookie]]];
}
WKUserScript *script = [[WKUserScript alloc]initWithSource:cookieScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
WKWebView *wkWebView = (WKWebView*)self.realWebView;
[wkWebView.configuration.userContentController addUserScript:script];
}
- (NSString *)javascriptStringWithCookie:(NSHTTPCookie*)cookie {
NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@;",
cookie.name,
cookie.value,
cookie.domain,
cookie.path ?: @"/"];
if (cookie.secure) {
string = [string stringByAppendingString:@"secure=true"];
}
return string;
}
# 4. 多个WKWebView之间共享Cookie
WKProcessPool
这个类用来配置进程池,与网页视图的资源共享有关。WKProcessPool
类中没有暴露任何属性和方法,所以拿不到任何数据。
为 多个WKWebView
配置为同一个WKProcessPool
,会让多个 WKWebView 之间共享数据,例如Cookie、用户凭证等。
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
configuration.processPool = [[WKWebViewPoolHandler sharedInstance] defaultPool];
# 5. 清除缓存
WebKit框架采用其本身的缓存框架,iOS 9 之后可以用WKWebsiteDataStore
类来清除缓存。
//WKWebsiteDataTypeDiskCache,
//WKWebsiteDataTypeOfflineWebApplicationCache,
//WKWebsiteDataTypeMemoryCache,
//WKWebsiteDataTypeLocalStorage,
//WKWebsiteDataTypeIndexedDBDatabases,
//WKWebsiteDataTypeWebSQLDatabases
NSSet *websiteDataTypes = [NSSet setWithArray:@[WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage]];
NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{}];
# 6. 处理被禁止的跳转
打开ituns.apple.com、跳转到appStore,、拨打电话,、唤起邮箱等一系列操作 WKWebView 不会自动交给UIApplication 来处理,除此之外,js端通过window.open() 打开新的网页的动作也被禁掉了
, 需要我们自己处理:
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
if ([url.scheme isEqualToString:@"tel"]){ // 打电话
if (IS_IOS_10Later) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}else {
[[UIApplication sharedApplication] openURL:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
if ([url.scheme isEqualToString:@"itms-services"]) { // 第三方下载
if (IS_IOS_10Later) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}else {
[[UIApplication sharedApplication] openURL:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
if ([url.absoluteString containsString:@"ituns.apple.com"]) { // 跳转到 appStroe
if (IS_IOS_10Later) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}else {
[[UIApplication sharedApplication] openURL:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
# 7.禁止长按出现复制框
在 webView 长按文字会出现下图效果:
解决方案:
[self.webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"=== 禁止用户选择 : %@", (error != nil) ? @"error" : @"succ");
}];
[self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"=== 禁止长按弹出框 : %@", (error != nil) ? @"error" : @"succ");
}];