虽然
WKWebView
是在Apple
的WWDC 2014
随iOS 8
和OS X 10.10
出来的,是为了解决UIWebView
加载速度慢、占用内存大的问题。但是由于之前还要适配iOS7
,又不想做两套加载页面(主要是因为懒),所以就没有使用。现在项目都适配iOS 8
以上了,所以就开始使用WKWebView
了,但是发现在使用的时候有好多坑,希望这篇文章能带大家绕过坑,更好的使用WKWebView
。
这篇文章主要介绍了以下问题,方便小伙伴们查阅:
- WKWebView的基本介绍和使用
- WKWebView和JavaScript的交互
- 解决WKWebView加载POST请求无法发送参数问题
WKWebView的基本介绍和使用
WKWebView的几个代理方法
WKWebView
是苹果在iOS 8
中引入的新组件,目的是给出一个新的高性能的WebView
解决方案,摆脱过去UIWebView
的老、旧、笨重,特别是内存占用量巨大的问题,它使用Nitro JavaScript
引擎,这意味着所有第三方浏览器运行JavaScript
将会跟safari
一样快。
看到我这篇文章的小伙伴,对iOS
的开发应该有一定的了解,肯定用过UIWebView
,现在就用UIWebView
和WKWebView
的代理方法做一个对比。
-
加载状态的回调
(用来跟踪页面加载的过程(页面开始加载、加载完成、加载失败的方法),还可以决定是否跳转)
:- 准备加载页面
UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
WKNavigationDelegate: - webView:didStartProvisionalNavigation:
2. **内容开始加载**`(view的过渡动画可在此方法中加载)`
UIWebViewDelegate: - webViewDidStartLoad:
WKNavigationDelegate: - webView:didCommitNavigation:
3. **页面加载完成**`(view的过渡动画的移除可在此方法中进行)`
UIWebViewDelegate: - webViewDidFinishLoad:
WKNavigationDelegate: - webView:didFinishNavigation:
4. **页面加载失败**
UIWebViewDelegate: - webView:didFailLoadWithError:
WKNavigationDelegate: - webView:didFailNavigation:withError:
WKNavigationDelegate: - webView:didFailProvisionalNavigation:withError:
此外,WKWebKit还有三个页面跳转
的代理方法:
- 页面跳转的代理
- 接收到服务器跳转请求的代理
WKNavigationDelegate: - webView:didReceiveServerRedirectForProvisionalNavigation:
2. **在收到响应后,决定是否跳转的代理**
WKNavigationDelegate: - webView:decidePolicyForNavigationResponse:decisionHandler:
3. **在发送请求之前,决定是否跳转的代理**
WKNavigationDelegate: - webView:decidePolicyForNavigationAction:decisionHandler:
WKWebView增加的属性
- WKWebViewConfiguration *configuration:初始化WKWebView的时候的配置,后面会用到
- WKBackForwardList *backForwardList:相当于访问历史的一个列表
- double estimatedProgress:进度,有这个之后就不用自己写假的进度条了
WKWebView的使用
OC代码:
// 创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"http://www.jianshu.com"];
// 根据URL创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加载请求
[webView loadRequest:request];
// 将WKWebView添加到视图
[self.view addSubview:webView];
Swift代码:
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 设置访问的URL
let url = NSURL(string: "http://www.jianshu.com")
// 根据URL创建请求
let requst = NSURLRequest(URL: url!)
// WKWebView加载请求
webView.loadRequest(requst)
// 将WKWebView添加到视图
view.addSubview(webView)
可以看到很简单,和UIWebView并没有多少差别,然而性能就刷刷刷的提上去了,是不是很爽呢?如果你只是简单的集成个Web页到App,这些已经够了。不过很多时候并没有那么简单,还需要处理各种东西,那么接着往后看。
WKWebView和JavaScript的交互
在
WebKit
框架中,有WKWebView
可以替换UIKit
的UIWebView
和AppKit
的WebView
,而且提供了在两个平台可以一致使用的接口。WebKit
框架使得开发者可以在原生App中使用Nitro
来提高网页的性能和表现,Nitro
就是Safari
的JavaScript
引擎,WKWebView
不支持JavaScriptCore
的方式但提供message handler
的方式为JavaScript
与Native通信。(这个引自天狐博客,更多的与UIWebView
或者WKWebView
的交互方法可以在这里看到。下面部分代码(例如JS)也是窃取这个作者的,尊重原著,所以把原博客地址放这里,与JS交互写的比我好多了。)
Native调用JavaScript
方法
原生调用JavaScript的代码需要在页面加载完成之后,就是在 - webView:didFinishNavigation:
代理方法里面
OC代码:
[webView evaluateJavaScript:@"showAlert('奏是一个弹框')" completionHandler:^(id item, NSError * _Nullable error) {
// Block中处理是否通过了或者执行JS错误的代码
}];
Swift代码:
webView.evaluateJavaScript("showAlert('奏是一个弹框')") { (item, error) in
// 闭包中处理是否通过了或者执行JS错误的代码
}
大家可以看到这段JS代码是最简单的弹出一个Alert的代码,后面WKWebView加载POST请求参数问题
中还会有一个加载POST请求的JS代码,先不要管它了,请各位看官继续往后翻,看看JavaScript
怎么调用Native的方法。
JavaScript
调用Native方法
-
JavaScript的配置
JavaScript
调用Native的方法就需要前端和Native的小伙伴们配合了,需要前端的小伙伴在JS的方法中调用:window.webkit.messageHandlers.NativeMethod.postMessage("就是一个消息啊");
这行代码。请注意,这个
NativeMethod
是和App中要统一的,配置方法将在下面的Native中书写。 -
Native App的代码配置
下面该Native的代码的配置了,细心的小伙伴可能已经发现了,创建
WKWebView
的时候,除了有- initWithFrame:
方法外,还有一个高端的方法:- initWithFrame:configuration:
方法。那句名言是谁说的来着:普通玩家选择推荐配置,高端玩家选择自定义配置
,就当是我说的吧(那个拿鞋的把鞋穿上吧,我承认不是我说的😂)。这个方法就是用来自定义配置的,具体怎么自定义呢,童鞋们接着往下看吧。OC代码:
// 创建配置 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; // 创建UserContentController(提供JavaScript向webView发送消息的方法) WKUserContentController* userContent = [[WKUserContentController alloc] init]; // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除 [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:@"http://www.jianshu.com"]; // 根据URL创建请求 NSURLRequest *request = [NSURLRequest requestWithURL:url]; // WKWebView加载请求 [webView loadRequest:request]; // 将WKWebView添加到视图 [self.view addSubview:webView];
Swift代码:
// 创建配置 let config = WKWebViewConfiguration() // 创建UserContentController(提供JavaScript向webView发送消息的方法) let userContent = WKUserContentController() // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除 userContent.addScriptMessageHandler(self, name: "NativeMethod") // 将UserConttentController设置到配置文件 config.userContentController = userContent // 高端的自定义配置创建WKWebView let webView = WKWebView(frame: UIScreen.mainScreen().bounds, configuration: config) // 设置访问的URL let url = NSURL(string: "http://www.jianshu.com") // 根据URL创建请求 let requst = NSURLRequest(URL: url!) // 设置代理 webView.navigationDelegate = self // WKWebView加载请求 webView.loadRequest(requst) // 将WebView添加到当前view view.addSubview(webView)
可以看到,添加消息处理的
handler
的name
,就是JavaScript
中调用时候的NativeMethod
,这两个要保持一致。请把URL换成你自己的。请注意第
6
行的代码配置当前ViewController
为MessageHandler
,需要服从WKScriptMessageHandler
协议,如果出现警告⚠️,请检查是否服从了这个协议。注意!注意!注意:上面将当前
ViewController
设置为MessageHandler
之后需要在当前ViewController
销毁前将其移除,否则会造成内存泄漏。移除的代码如下:
OC代码:
[webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];
Swift代码:
webView.configuration.userContentController.removeScriptMessageHandlerForName("NativeMethod")
请注意这个
Name
和上面创建WKWebView
的配置中注册的名字是一样的,要保持对应。好了,现在万事俱备,只欠东风了。东风是什么呢,就是该在哪儿处理。可以看到
WKScriptMessageHandler
的协议里面只有一个方法,就是:- userContentController:didReceiveScriptMessage:
相信聪明的你已经猜到了。是的,就是在这个代理方法里面操作:如果
JavaScript
执行已经写好的:window.webkit.messageHandlers.NativeMethod.postMessage("就是一个消息啊");
这行代码,这个代理方法就会走,并且会有个WKScriptMessage
的对象,这个WKScriptMessage
对象有个name
属性,拿到之后你会发现,就是我们注册的NativeMethod
这个字符串,这时候你就可以手动调用Native的方法了。如果有多个方法需要调用的话怎么办,看到JavaScript
中postMessage()
方法有一个参数了没有,可以根据这里的参数来区分调用原生App的哪个方法。
代码很简单,就不写了。什么?你说你还需要写?好吧,那我还是贴出来吧:OC代码:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { // 判断是否是调用原生的 if ([@"NativeMethod" isEqualToString:message.name]) { // 判断message的内容,然后做相应的操作 if ([@"close" isEqualToString:message.body]) { } } }
Swift代码:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { // 判断是否是调用原生的 if "NativeMethod" == message.name { // 判断message的内容,然后做相应的操作 if "close" == message.body as! String { } } }
上面的方法就可以获取到
JavaScript
发送的Message了,JavaScript
可以这样调用:window.webkit.messageHandlers.NativeMethod.postMessage("close");
,这时候上面的代理方法的两个if
判断都能通过,不同的操作可增加里面的if
语句的分支判断message的内容来进行不同的Native代码的调用,也就是JavaScript
的postMessage
方法的参数的不同来区分不同的操作。好了,现在
WKWebView
和JavaScript
的简单交互你也会了。用WKWebView
的时候貌似也还算开心。但是不要高兴的太早,下面就要有坑了。
解决WKWebView加载POST请求无法发送参数问题
也许你用
UIWebView
加载过POST请求的页面,感觉并没有什么难点或者需要注意的地方,那真的是图样图森破了,因为我也这样天真过。直到我踩了很多坑之后,我才发现梦想与现实之间的差别,不过没关系,我又要说另一句名言了:没有挖不到的墙角...,咳咳咳,说错了,请重新来BGM,跟我一起说:没有解决不了的Bug,只有不努力的码农!
(各位架构师、高级开发工程师请手下留情,我说的码农是我😂)
来来来,先来一发POST
请求加载WebView
。你会说,这还不easy?下面就来一个,走起:
OC代码:
// 创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
// 根据URL创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求方法为POST
[request setHTTPMethod:@"POST"];
// WKWebView加载请求
[webView loadRequest:request];
// 将WKWebView添加到视图
[self.view addSubview:webView];
Swift代码:
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 设置访问的URL
let url = NSURL(string: "http://www.example.com")
// 根据URL创建请求
let requst = NSMutableURLRequest(URL: url!)
// 设置请求方法为POST
requst.HTTPMethod = "POST"
// WKWebView加载请求
webView.loadRequest(requst)
// 将WKWebView添加到视图
view.addSubview(webView)
这样确实加载POST
请求的网页成功了(注意请把链接换成自己的),你一定露出了得意的笑容。但是骚年,不要高兴的太早,这只是一个简单的POST请求,还没有添加参数呢。于是乎,你又说:那更简单,在第9
行插入如下代码即可(比方说这个接口是登录):
OC代码:
// 设置请求参数
[request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
Swift代码:
// 设置请求参数
requst.HTTPBody = "username=aaa&password=123".dataUsingEncoding(NSUTF8StringEncoding)
这种方法在UIWebView
里面是没有问题的,所以你认为在这里也应该是没有问题的。从理论上讲应该是这样的,但是我要恭喜你了,这是WKWebView
的Bug,让你给碰到了。这里写的POST
请求没有问题,但是就是不会把这两个参数传上去的,不信你可以试试(截止我发表这篇博客的日期,iOS 9.3
并没有修复此问题)。
好了,不废话了(其实已经说了很多废话了),下面看解决办法(如果你需要适配iOS 8
请直接使用方法2
):
- 使用
NSURLSession
发送一个请求,然后把请求下来的数据当作本地HTML
加载 - 使用
JavaScript
解决WKWebView
无法发送POST
参数问题
1. 使用NSURLSession
解决WKWebView
无法POST
参数的问题(性能和结果都可能有问题,不推荐使用)
当发现POST
无法传递参数的时候,我首先想到的是换个方法来,就是用一般的请求方式:NSURLSession
发送请求,然后把接收到的数据转化成字符串,然后再用WKWebView
加载。大家可能已经看出来了,需要把整个网页放到内存中或着放到本地然后再加载,所以肯定消耗内存呀。下面贴代码吧:
OC代码:
// 创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 将WKWebView添加到当前View
[self.view addSubview:webView];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
// 根据URL创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求方法为POST
[request setHTTPMethod:@"POST"];
// 设置请求参数
[request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
// 实例化网络会话
NSURLSession *session = [NSURLSession sharedSession];
// 创建请求Task
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 将请求到的网页数据用loadHTMLString 的方法加载
NSString *htmlStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[webView loadHTMLString:htmlStr baseURL:nil];
}];
// 开启网络任务
[task resume];
Swift代码:
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 设置访问的URL
let url = NSURL(string: "http://www.example.com")
// 根据URL创建请求
let requst = NSMutableURLRequest(URL: url!)
// 设置请求方法为POST
requst.HTTPMethod = "POST"
// 设置请求参数
requst.HTTPBody = "username=aaa&password=123".dataUsingEncoding(NSUTF8StringEncoding)
// 将WKWebView添加到视图
view.addSubview(webView)
// 实例化网络会话
let session = NSURLSession.sharedSession()
// 创建请求Task
let task = session.dataTaskWithRequest(requst) { (data, response, error) in
webView.loadHTMLString(String(data: data!, encoding: NSUTF8StringEncoding)!, baseURL: nil)
}
task.resume()
当你用iOS 9
以上的设备的时候,貌似完全没有一点问题,只是需要请求下来再放而已。但是注意前提条件:iOS 9
,当你用iOS 8
的时候,发现你的网页的样式和JavaScript
事件全部没有了。是不是有一种呵呵的冲动,那你就尽情呵呵吧。如果你要适配iOS 8
,那么这个方法也不符合你的气质。
其实这个东西和加载本地网页无法加载CSS样式和JS一样,如果你也加载本地HTML文件出现问题,请查看Jay神
的WKWebView使用遇到的坑。尽给别人打广告了,呵呵,声明一下啊:我跟这些人木有关系,只是为了方便大家查阅而已,谁让我那么的大公无私呢😂。
好了,好了,来看一个更好的解决办法吧:
2. 使用JavaScript
解决WKWebView
无法发送POST
参数问题
开始之前我先说一下实现思路,方便大家理解,如果出错了也能知道错误的地方:
- 将一个包含
JavaScript
的POST
请求的HTML
代码放到工程目录中- 加载这个包含
JavaScript
的POST
请求的代码到WKWebView
- 加载完成之后,用Native调用
JavaScript
的POST
方法并传入参数来完成请求
-
创建包含
JavaScript
的POST
请求的HTML
代码相关代码:
<html> <head> <script> //调用格式: post('URL', {"key": "value"}); 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>
```
将这段代码拷贝下来,然后粘贴到文本编辑器中,名字可以随意起,比方说保存为:JSPOST.html
,然后拷贝到工程目录中,记得选择对应的Target和勾选Copy items if needed
(默认应该是勾选的)。这时候,就可以用这段JavaScript
代码来发送带参数的POST
请求了。
-
将对应的
JavaScript
代码通过加载本地网页的形式加载到WKWebView
OC代码:
// JS发送POST的Flag,为真的时候会调用JS的POST方法(仅当第一次的时候加载本地JS) self.needLoadJSPOST = YES; // 创建WKWebView self.webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds]; //设置代理 self.webView.navigationDelegate = self; // 获取JS所在的路径 NSString *path = [[NSBundle mainBundle] pathForResource:@"JSPOST" ofType:@"html"]; // 获得html内容 NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; // 加载js [self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]]; // 将WKWebView添加到当前View [self.view addSubview:self.webView];
Swift代码:
// JS发送POST的Flag,为真的时候会调用JS的POST方法(仅当第一次的时候加载本地JS) needLoadJSPOST = true // 创建WKWebView webView = WKWebView(frame: UIScreen.mainScreen().bounds) //设置代理 webView.navigationDelegate = self // 获取JS路径 let path = NSBundle.mainBundle().pathForResource("JSPOST", ofType: "html") // 获得html内容 do { let html = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding) // 加载js webView.loadHTMLString(html, baseURL: NSBundle.mainBundle().bundleURL) } catch { } // 将WKWebView添加到当前View view.addSubview(webView)
这段代码就相当于把工程中的
JavaScript
脚本加载到WKWebView
中了,后面就是看怎么用了。(请注意换成您的文件名) -
Native调用
JavaScript
脚本并传入参数来完成POST
请求还记得
WKWebView和JavaScript的交互
这一节嘛?现在该Native调用JavaScript
了,如果忘记了,请往前翻温故一下:- webView:didFinishNavigation:
代理表明页面已经加载完成,我们在这里操作,下面上代码:OC代码:
// 加载完成的代理方法 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { // 判断是否需要加载(仅在第一次加载) if (self.needLoadJSPOST) { // 调用使用JS发送POST请求的方法 [self postRequestWithJS]; // 将Flag置为NO(后面就不需要加载了) self.needLoadJSPOST = NO; } } // 调用JS发送POST请求 - (void)postRequestWithJS { // 发送POST的参数 NSString *postData = @"\"username\":\"aaa\",\"password\":\"123\""; // 请求的页面地址 NSString *urlStr = @"http://www.postexample.com"; // 拼装成调用JavaScript的字符串 NSString *jscript = [NSString stringWithFormat:@"post('%@', {%@});", urlStr, postData]; // NSLog(@"Javascript: %@", jscript); // 调用JS代码 [self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) { }]; }
Swift代码:
// 加载完成的代理方法 func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) { // 判断是否需要加载(仅在第一次加载) if needLoadJSPOST { // 调用使用JS发送POST请求的方法 postRequestWithJS() // 将Flag置为NO(后面就不需要加载了) needLoadJSPOST = false } } // 调用JS发送POST请求 func postRequestWithJS() { // 发送POST的参数 let postData = "\"username\":\"aaa\",\"password\":\"123\"" // 请求的页面地址 let urlStr = "http://www.postexample.com" // 拼装成调用JavaScript的字符串 let jscript = "post('\(urlStr)', {\(postData)});" // 调用JS代码 webView.evaluateJavaScript(jscript) { (object, error) in } }
好了,到目前为止你的请求就发出去了。相信后面的版本会解决这个问题,但是现在你要用的话也得有办法,谁让已经入了
Apple
的坑呢,谁让UIWebView
太不给力了呢.
写在最后:
当时选择WKWebView
就是为了提高性能,但是没有想到遇到这么多坑,从看iOS 9
才解决了iOS 8
无法加载本地样式的问题,有时候苹果解决问题的速度还有略慢的,到现在POST
请求参数都发不出去也真是不应该。不过没办法,先解决了,说不定iOS 10
出来之后解决了呢。(我虽然有iOS 10
的设备,但是我还没有测试,感兴趣的小伙伴们可以试试)。大家如果有什么问题,欢迎留言提问。谢谢支持!