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


https://www.jianshu.com/p/403853b63537

 

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

 

作者:winann

链接:https://www.jianshu.com/p/403853b63537

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM