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

96
winann
2.9 2016.07.01 15:53* 字数 4455

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

这篇文章主要介绍了以下问题,方便小伙伴们查阅:

  1. WKWebView的基本介绍和使用
  1. WKWebViewJavaScript的交互
  1. 解决WKWebView加载POST请求无法发送参数问题

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


iOS开发
Web note ad 1