WKWebView
是在Apple
的WWDC 2014
随iOS 8
和OS X 10.10
出来的,是为了解决UIWebView
加载速度慢、占用内存大的问题。但是由于之前还要适配iOS7,所以就没有使用。现在项目都适配iOS 8以上了,所以就开始使用WKWebView
了,但是发现在使用的时候有好多坑
文章目录
1.WKWebView的基本介绍和使用
1.1 创建 跟UIWebview一样
// 创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665"];
// 根据URL创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加载请求
[webView loadRequest:request];
// 将WKWebView添加到视图
[self.view addSubview:webView];
1.2 UIWebView和WKWebView的代理方法做一个对比
1.2.1.准备加载页面
UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
WKNavigationDelegate: - webView:didStartProvisionalNavigation:
1.2.2.内容开始加载
UIWebViewDelegate: - webViewDidStartLoad:
WKNavigationDelegate: - webView:didCommitNavigation:
1.2.3.页面加载完成
UIWebViewDelegate: - webViewDidFinishLoad:
WKNavigationDelegate: - webView:didFinishNavigation:
1.2.4.页面加载失败
UIWebViewDelegate: - webView:didFailLoadWithError:
WKNavigationDelegate: - webView:didFailNavigation:withError:
WKNavigationDelegate: - webView:didFailProvisionalNavigation:withError:
可以看到很简单,和UIWebView
并没有多少差别,然而性能提高很多,如果你只是简单的集成个Web页到App,这些已经够了。不过很多时候并没有那么简单,还需要处理各种东西,那么接着往后看。
2.WKWebView和JavaScript的交互
WKWebView
和JavaScript
的交互主要涉及到两个方面,
- 一个是
OC
调用JavaScript
, - 另一个是
JavaScript
调用OC
的方法,
在
WebKit
框架中,有WKWebView
可以替换UIKit
的UIWebView
和AppKit
的WebView
,而且提供了在两个平台可以一致使用的接口。WebKit框架
使得开发者可以在原生App中使用Nitro
来提高网页的性能和表现,Nitro
就是Safari
的JavaScript引擎
,WKWebView
不支持JavaScriptCore
的方式但提供message handler
的方式为JavaScript
与Native
通信。
2.1. OC调用JavaScript
OC调用JavaScrippt
是相对来说比较简单的,只需要在调用的地方添加下面一句代码即可:
//showAlert()是js里面的方法,这样就可以实现调用js方法
[self.webView evaluateJavaScript:@"showAlert('参数')" completionHandler:^(id item, NSError * _Nullable error) {
// Block中处理是否通过了或者执行JS错误的代码
}];
2.2. JavaScript 调用OC的方法,相对来说复杂一点
这地方需要两个配置,一个是OC代码的配置,另一个是JS代码的配置,下面先说一下OC代码的配置,细心的小伙伴可能已经发现了,创建WKWebView的时候,除了有- initWithFrame:
方法外,还有一个高端的方法:- initWithFrame:configuration:
方法。
2.2.1.配置 WKWebView
// 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
//NativeMethod 这个方法一会要与JS里面的方法写的一样
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
// 将UserConttentController设置到配置文件
config.userContentController = userContent;
// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665?"];
// 根据URL创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加载请求
[webView loadRequest:request];
// 将WKWebView添加到视图
[self.view addSubview:webView];
2.2.2.实现协议方法
好了,现在万事俱备,只欠东风了。东风是什么呢,就是该在哪儿处理。可以看到WKScriptMessageHandler
的协议里面只有一个方法,就是:
- userContentController:didReceiveScriptMessage:
就是在这个代理方法里面操作:如果JavaScript
执行已经写好的:window.webkit.messageHandlers.NativeMethod.postMessage("就是一个桂呀")
;这行代码,这个代理方法就会走,并且会有个WKScriptMessage的对象,这个WKScriptMessage
对象有个name属性,拿到之后你会发现,就是我们注册的NativeMethod
这个字符串,这时候你就可以手动调用Native
的方法了。如果有多个方法需要调用的话怎么办,看到JavaScript
中postMessage()
方法有一个参数了没有,可以根据这里的参数来区分调用原生App的哪个方法。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判断是否是调用原生的
if ([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if ([@"close" isEqualToString:message.body]) {
}
}
}
2.2.3.JavaScript的配置
JavaScript
调用Native
的方法就需要前端和Native
的小伙伴们配合了,需要前端的小伙伴在JS的方法中调用:
window.webkit.messageHandlers.方法名.postMessage(参数)
注意:
参数没有时传 (null)
这里是个坑点
- 第一:实现以上代码的时候不要忘记实现** WKScriptMessageHandler**协议
- 第二:上面将当前
ViewController
设置为MessageHandler
之后需要在当前ViewController
销毁前将其移除(dealloc方法)
,否则会造成内存泄漏。
//页面进入时创建
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:JS_goPageSelectClass];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:JS_goClasscardList];
}
//页面消失是移除
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:JS_goPageSelectClass];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:JS_goClasscardList];
}
js
区分Android
和iOS
的方法
var u = navigator.userAgent;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //iOS终端
if(isAndroid){
window.Android.alipayOrder();
}
if(isiOS){
window.webkit.messageHandlers.alipayOrder.postMessage(r);
}
3.WKWebView 默认不弹出js的alert问题
WKWebview
默认是不弹出js
的alert
要想可以弹出alert
需要手动的设置代理实现
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
协议方法
具体的实现方法是,我们采用源生的UIAlertController 来实现弹出框,获取js里面的alert内容显示出来
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
NSLog(@"取消按钮==%@",message);
}])];
[alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
NSLog(@"确定按钮==%@",message);
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
4.WKWebView 默认是不能识别电话号码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
具体实现:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
NSURL *URL = navigationAction.request.URL;
NSLog(@"获取到=====%@",URL);
NSString *scheme = [URL scheme];
UIApplication *app = [UIApplication sharedApplication];
// 打电话
if ([scheme isEqualToString:@"tel"]) {
if ([app canOpenURL:URL]) {
[app openURL:URL];
// 一定要加上这句,否则会打开新页面
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
注:对应html代码采用的是a标签
<a href="tel:16606668888">识别电话号码16606668888,进行拨打电话</a>
5.WKWebView 拦截js通过window.open() 打开的窗口
- (WKWebView )webView:(WKWebView )webView createWebViewWithConfiguration:(WKWebViewConfiguration )configuration forNavigationAction:(WKNavigationAction )navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
会拦截到window.open()事件.
只需要我们在在方法内进行处理
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
6.WKWebView解决文字显示太小问题
在使用WKWebView
的时候,常常会碰到显示内容比实际css设置的样式不能正常显示,内容普遍的偏小。其实导致这样问题的根源是少了HTML5
的meta标签。解决的办法可以在iOS端添加以下的内容,当然也可以让后台添加完整的HTML5的格式。如果要在iOS端指定字体的大小也是可以的(不推荐在客户端设置字体大小)。
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
_myWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0,CGRectGetMaxY(headerView.frame)+10, M_S.width,M_S.height - CGRectGetMaxY(headerView.frame) - 40) configuration:wkWebConfig];
客户端设置字体大小eg:
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
//修改字体大小 300%
[ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '300%'" completionHandler:nil];
// //修改字体颜色 #6566b6
// [ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextFillColor= '#6566b6'" completionHandler:nil];
}
7.WKWebView 几个不常用的特性
iOS 中对 Web 的支持可以分为两个阶段:UIWebView 以及后来的 WKWebView。自 iOS 12 起,
用。而在不久的将来,Apple 甚至不接受带有 UIWebView 的应用程序提交。UIWebView
就开始被弃WKWebView
是WebKit
框架的一部分,在应用程序的主线程之外运行,从而有助于其稳定性和卓越的性能。
首先,要加载内容,我们只需执行以下操作:
guard let url = URL(string: string) else { return }
letrequest = URLRequest(url: url)
webView?.load(request)
除了内容加载和 CSS 样式外,WKWebView
还可以做很多事情。
以下部分是WKWebView
相对用得较少的一些功能清单。
7.1 .截获 Web URL
通过实现WKNavigationDelegate
协议的 definePolicyFor
函数,我们可以在导航期间截获URL
。以下代码段显示了如何完成此操作:
funcwebView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let urlString = navigationAction.request.url?.absoluteString ?? ""
let pattern = "interceptSomeUrlPattern"
if urlString.contains(pattern){
var splitPath = urlString.components(separatedBy: pattern)
}
}
7.2 JavaScript Alert
默认情况下,来自JavaScript
的提示不会显示在WKWebView
中,因为它不是 UIKit 的一部分。因此,我们需要实现 WKUIDelegate
协议,以便显示提示信息中的警告、确认或文本输入等。
以下几种提示类型对应的方法:
funcwebView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void)
funcwebView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void)
funcwebView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
let alertController = UIAlertController(title: nil, message: prompt, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.text = defaultText
}
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action) in
iflet text = alertController.textFields?.first?.text {
completionHandler(text)
} else {
completionHandler(defaultText)
}
}))
self.present(alertController, animated: true, completion: nil)
}
7.3 配置URL操作
使用decisionPolicyFor
函数,您不仅可以通过电话,facetime
和邮件等操作来控制外部导航,还可以选择限制某些 URL 的打开。以下代码展示了每种情况:
funcwebView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guardlet url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
if ["tel", "sms", "mailto"].contains(url.scheme) && UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
} else {
iflet host = navigationAction.request.url?.host {
if host == "www.wkwebview.com" {
decisionHandler(.cancel)
}
else{
decisionHandler(.allow)
}
}
}
}
}
7.4 使用 WKWebView 进行身份验证
当WKWebView
中的 URL 需要用户授权时,您需要实现以下方法:
funcwebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let authenticationMethod = challenge.protectionSpace.authenticationMethod
if authenticationMethod == NSURLAuthenticationMethodDefault || authenticationMethod == NSURLAuthenticationMethodHTTPBasic || authenticationMethod == NSURLAuthenticationMethodHTTPDigest {
//Do you stuff
}
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
}
收到身份验证质询后,您可以确定所需的身份验证类型(用户凭据或证书),并相应地使用提示或预定义凭据来处理条件。
7.5 多个 WKWebView 共享 Cookie
WKWebView
的每个实例都有其自己的cookie
存储。为了在 WKWebView
的多个实例之间共享 cookie
,我们需要使用 WKHTTPCookieStore
,如下所示:
let cookies = HTTPCookieStorage.shared.cookies ?? []
for (cookie) in cookies {
webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}
7.6 获取加载进度
WKWebView
的其他功能非常普遍,例如显示正在加载的 URL 的进度更新。
可以通过侦听以下方法的 estimatedProgress
的 keyPath
值来更新 ProgressViews:
overridefuncobserveValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
8. 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 2021 23:38:25 GMT;
通过 WKWebView
发起请求http://qq.com
, 请求头不会自动带上cookie: Nicholas=test
。
8.2 解决办法
由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:
WKWebView loadRequest
前,在request header
中设置Cookie
, 解决首个请求Cookie
带不上的问题;
WKWebView * webView = [WKWebView new];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://blog.csdn.net/sinat_35487665?spm=1000.2115.3001.5343"]];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
- 通过 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.ba.com
,我们通过在 request header 里带上 Cookie
解决该请求的 Cookie 问题,接着页面302跳转到 www.bd.com,这个时候 www.bc.com
这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
可以在该回调函数里拦截302请求,copy request
,在 equest header
中带上cookie
并重新 loadRequest
。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]
只适合加载 mainFrame
请求。