WKWebView的使用和各种坑的解决方法(OC+Swift)

/ Mac / 没有评论 / 1395浏览

WKWebView的使用和各种坑的解决方法(OC+Swift)

虽然WKWebView是在AppleWWDC 2014iOS 8OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。但是由于之前还要适配iOS7,又不想做两套加载页面(主要是因为懒),所以就没有使用。现在项目都适配iOS 8以上了,所以就开始使用WKWebView了,但是发现在使用的时候有好多坑,希望这篇文章能带大家绕过坑,更好的使用WKWebView

WKWebView的基本介绍和使用

WKWebView的几个代理方法

WKWebView是苹果在iOS 8中引入的新组件,目的是给出一个新的高性能的WebView解决方案,摆脱过去 UIWebView的老、旧、笨重,特别是内存占用量巨大的问题,它使用Nitro JavaScript引擎,这意味着所有第三方浏览器运行JavaScript将会跟safari一样快。

看到我这篇文章的小伙伴,对iOS的开发应该有一定的了解,肯定用过UIWebView,现在就用UIWebViewWKWebView的代理方法做一个对比。

加载状态的回调

(用来跟踪页面加载的过程(页面开始加载、加载完成、加载失败的方法),还可以决定是否跳转:

1. 准备加载页面

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还有三个页面跳转的代理方法:

页面跳转的代理

1. 接收到服务器跳转请求的代理

WKNavigationDelegate: - webView:didReceiveServerRedirectForProvisionalNavigation:

2. 在收到响应后,决定是否跳转的代理

WKNavigationDelegate: - webView:decidePolicyForNavigationResponse:decisionHandler:

3. 在发送请求之前,决定是否跳转的代理

WKNavigationDelegate: - webView:decidePolicyForNavigationAction:decisionHandler:

WKWebView增加的属性

  1. WKWebViewConfiguration *configuration:初始化WKWebView的时候的配置,后面会用到
  2. WKBackForwardList *backForwardList:相当于访问历史的一个列表
  3. 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,这些已经够了。不过很多时候并没有那么简单,还需要处理各种东西,那么接着往后看。

WKWebViewJavaScript的交互

WebKit框架中,有WKWebView可以替换UIKitUIWebViewAppKitWebView,而且提供了在两个平台可以一致使用的接口。WebKit框架使得开发者可以在原生App中使用Nitro来提高网页的性能和表现,Nitro就是SafariJavaScript引擎,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)

可以看到,添加消息处理的handlername,就是JavaScript中调用时候的NativeMethod,这两个要保持一致。请把URL换成你自己的。

请注意第6行的代码配置当前ViewControllerMessageHandler,需要服从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的方法了。如果有多个方法需要调用的话怎么办,看到JavaScriptpostMessage()方法有一个参数了没有,可以根据这里的参数来区分调用原生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代码的调用,也就是JavaScriptpostMessage方法的参数的不同来区分不同的操作。

好了,现在WKWebViewJavaScript的简单交互你也会了。用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):

  1. 使用NSURLSession发送一个请求,然后把请求下来的数据当作本地HTML加载
  2. 使用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参数问题

开始之前我先说一下实现思路,方便大家理解,如果出错了也能知道错误的地方:

  1. 将一个包含JavaScriptPOST请求的HTML代码放到工程目录中
  2. 加载这个包含JavaScriptPOST请求的代码到WKWebView
  3. 加载完成之后,用Native调用JavaScriptPOST方法并传入参数来完成请求

1. 创建包含JavaScriptPOST请求的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请求了。

1. 将对应的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中了,后面就是看怎么用了。(请注意换成您的文件名)

2. 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的设备,但是我还没有测试,感兴趣的小伙伴们可以试试)。大家如果有什么问题,欢迎留言提问。谢谢支持!