WKWebView详解&WKWebVieW和JS交互

6,940 阅读11分钟

开发App的过程中,常常会遇到在App内部加载网页,通常用UIWebView加载。而这个自iOS2.0开始使用的Web容器一直是开发的心病:加载速度慢,占用内存多,优化困难。如果加载网页多,还可能因为过量占用内存而给系统kill掉。各种优化的方法效果也不那么明显iOS8 以后,苹果推出了新框架 WebKit,提供了替换 UIWebView 的组件 WKWebView。各种 UIWebView 的性能问题没有了,速度更快了,占用内存少了,体验更好了,下面列举一些其它的优势: 1、在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存,模拟器加载百度与开源中国网站时,WKWebView占用23M,而UIWebView占用85M); 2、允许JavaScript的Nitro库加载并使用(UIWebView中限制); 3、支持了更多的HTML5特性; 4、高达60fps的滚动刷新率以及内置手势; 5、将UIWebViewDelegate与UIWebView重构成了14类与3个协议(查看苹果官方文档);

14个类

WKBackForwardList: 之前访问过的 web 页面的列表,可以通过后退和前进动作来访问到。 WKBackForwardListItem: webview 中后退列表里的某一个网页。 WKFrameInfo: 包含一个网页的布局信息。 WKNavigation: 包含一个网页的加载进度信息。 WKNavigationAction: 包含可能让网页导航变化的信息,用于判断是否做出导航变化。 WKNavigationResponse: 包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。 WKPreferences: 概括一个 webview 的偏好设置。 WKProcessPool: 表示一个 web 内容加载池。 WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。 WKScriptMessage: 包含网页发出的信息。 WKUserScript: 表示可以被网页接受的用户脚本。 WKWebViewConfiguration: 初始化 webview 的设置。 WKWindowFeatures: 指定加载新网页时的窗口属性。

3个协议

WKNavigationDelegate: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。 WKUIDelegate: 提供用原生控件显示网页的方法回调。 WKScriptMessageHandler: 提供从网页中收消息的回调方法。

所有相关的类的API

//上文介绍过的偏好配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
// 导航代理 
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// 用户交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;

// 页面前进、后退列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

// 默认构造器
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;


//加载请求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

// 加载URL
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);

// 直接加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

// 直接加载data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);

// 前进或者后退到某一页面
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

// 页面的标题,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSString *title;

// 当前请求的URL,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSURL *URL;

// 标识当前是否正在加载内容中,支持KVO的
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

// 当前加载的进度,范围为[0, 1]
@property (nonatomic, readonly) double estimatedProgress;

// 标识页面中的所有资源是否通过安全加密连接来加载,支持KVO的
@property (nonatomic, readonly) BOOL hasOnlySecureContent;

// 当前导航的证书链,支持KVO
@property (nonatomic, readonly, copy) NSArray *certificateChain NS_AVAILABLE(10_11, 9_0);

// 是否可以招待goback操作,它是支持KVO的
@property (nonatomic, readonly) BOOL canGoBack;

// 是否可以执行gofarward操作,支持KVO
@property (nonatomic, readonly) BOOL canGoForward;

// 返回上一页面,如果不能返回,则什么也不干
- (nullable WKNavigation *)goBack;

// 进入下一页面,如果不能前进,则什么也不干
- (nullable WKNavigation *)goForward;

// 重新载入页面
- (nullable WKNavigation *)reload;

// 重新从原始URL载入
- (nullable WKNavigation *)reloadFromOrigin;

// 停止加载数据
- (void)stopLoading;

// 执行JS代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

// 标识是否支持左、右swipe手势是否可以前进、后退
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;

// 自定义user agent,如果没有则为nil
@property (nullable, nonatomic, copy) NSString *customUserAgent NS_AVAILABLE(10_11, 9_0);

// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0);

#if TARGET_OS_IPHONE
/*! @abstract The scroll view associated with the web view.
 */
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
#endif

#if !TARGET_OS_IPHONE
// 标识是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;

// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;

// 根据设置的缩放因子来缩放页面,并居中显示结果在指定的点
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;

#使用 ####1、首先需要先引入WebKit库

#import <WebKit/WebKit.h>

####2、两种初始化方法

// 默认初始化
- (instancetype)initWithFrame:(CGRect)frame;

// 根据对webview的相关配置,进行初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;

3、加载网页

最基础的方法和UIWebView一样

NSURL *url = [NSURL URLWithString:@"www.jianshu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];

一些其他的加载方法

//加载本地URL文件
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL 
               allowingReadAccessToURL:(NSURL *)readAccessURL

//加载本地HTML字符串
- (nullable WKNavigation *)loadHTMLString:(NSString *)string
                                  baseURL:(nullable NSURL *)baseURL;
//加载二进制数据
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL

4、代理方法

1、【WKNavigationDelegate协议】

该代理提供的方法,可以用来追踪加载过程(页面开始加载、加载完成、加载失败)、决定是否执行跳转。

// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;

页面跳转的代理方法有三种,分为(收到跳转与决定是否跳转两种)

// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到响应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
2、【WKUIDelegate协议】

WKUIDelegate从名称能看出它是webView在user interface上的代理,共有5个可选类型的代理方法。它为webView提供了原生的弹框,而不是JavaScript里的提示框。虽然JavaScript的提示框可以做的跟原生一样,但是对于ios开发者来说,如果要更改提示框就不方便了。提供这个代理,可以让ios端更加灵活的修改提示框的样式。

// 新建WKWebView
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

// 关闭WKWebView
- (void)webViewDidClose:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);

// 对应js的Alert方法
/**
 *  web界面中有弹出警告框时调用
 *
 *  @param webView           实现该代理的webview
 *  @param message           警告框中的内容
 *  @param frame             主窗口
 *  @param completionHandler 警告框消失调用
 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

// 对应js的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

// 对应js的prompt方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler;
3、【WKScriptMessageHandler】

这个协议中包含一个必须实现的方法,这个方法是native与web端交互的关键,它可以直接将接收到的JS脚本转为OC或Swift对象。

// 从web界面中接收到一个脚本时调用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

重点之WKWebVieW和JS交互

HTML代码

<html>
    <!--描述网页信息-->
    <head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>title</title>
 		<style>
            *{
                font-size: 50px;
            }
        
            .btn{height:80px; width:80%; padding: 0px 30px; background-color: #0071E7; border: solid 1px #0071E7; border-radius:5px; font-size: 1em; color: white}
 		</style>
        
        <script>
            
        
            //OC调用JS的方法列表
        	function alertMobile() {
                document.getElementById('mobile').innerHTML = '不带参数'
        	}

        	function alertName(msg) {
                //有一个参数
                document.getElementById('name').innerHTML = '有一个参数 :' + msg
        	}

        	function alertSendMsg(num,msg) {
        		//有两个参数
                document.getElementById('msg').innerHTML = '有两个参数:' + num + ',' + msg + '!!'
            }
        
            //JS响应方法列表
            function btnClick1() {
                window.webkit.messageHandlers.showMobile.postMessage(null)
            }

            function btnClick2() {
                window.webkit.messageHandlers.showName.postMessage('有一个参数')
            }

            function btnClick3() {
                window.webkit.messageHandlers.showSendMsg.postMessage(['两个参数One', '两个参数Two'])
            }

        </script>
        
        
    </head>

    <!--网页具体内容-->
    <body>
        <br/>

		<div>
			<label>WKWebView&JS交互</label>
		</div>
		<br/>

        <div id="mobile"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick1()">不带参数</button>
		</div>
		<br/>
        
        <div id="name"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick2()">一个参数</button>
		</div>
		<br/>
        
        <div id="msg"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick3()">两个参数</button>
		</div>


    </body>
</html>

OC代码

需要的头文件

#import <WebKit/WebKit.h>

需要遵守的代理

1、设置偏好设置,以及JS调用OC 添加处理脚本,这里的内容我写在了viewDidLoad里面,但是需要注意的是,需要在我们结束的时候释放WKUserContentController,不然会造成内存泄漏

- (void)viewDidLoad {
    [super viewDidLoad];
    // 设置偏好设置
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    // 默认为0
    config.preferences.minimumFontSize = 10;
    //是否支持JavaScript
    config.preferences.javaScriptEnabled = YES;
    //不通过用户交互,是否可以打开窗口
    config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
    
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height/2) configuration:config];
    [self.view addSubview:self.webView];

    
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
    [self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];
    
    WKUserContentController *userCC = config.userContentController;
    //JS调用OC 添加处理脚本
    [userCC addScriptMessageHandler:self name:@"showMobile"];
    [userCC addScriptMessageHandler:self name:@"showName"];
    [userCC addScriptMessageHandler:self name:@"showSendMsg"];
    
}

2、释放WKUserContentController代码

-(void)removeAllScriptMsgHandle{
    WKUserContentController *controller = self.webView.configuration.userContentController;
    [controller removeScriptMessageHandlerForName:@"showMobile"];
    [controller removeScriptMessageHandlerForName:@"showName"];
    [controller removeScriptMessageHandlerForName:@"showSendMsg"];
}

在JS调用OC以后会走的代理

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    NSLog(@"%@",message.body);
    
    if ([message.name isEqualToString:@"showMobile"]) {
        [self showMsg:@"没有参数"];
    }
    
    if ([message.name isEqualToString:@"showName"]) {
        NSString *info = [NSString stringWithFormat:@"%@",message.body];
        [self showMsg:info];
    }
    
    if ([message.name isEqualToString:@"showSendMsg"]) {
        NSArray *array = message.body;
        NSString *info = [NSString stringWithFormat:@"有两个参数: %@, %@ !!",array.firstObject,array.lastObject];
        [self showMsg:info];
    }
}

网页加载完成之后调用JS代码才会执行,因为这个时候html页面已经注入到webView中并且可以响应到对应方法。OC调用JS代码

//不带参数
- (IBAction)NOParameter:(id)sender {
    [self.webView evaluateJavaScript:@"alertMobile()" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        //JS 返回结果
        NSLog(@"%@ %@",response,error);
    }];
}
//一个参数
- (IBAction)oneParameter:(id)sender {
    /*
     *alertName('奥特们打小怪兽')
     *alertName JS方法名
     *奥特们打小怪兽 带的参数
     */
    [self.webView evaluateJavaScript:@"alertName('奥特们打小怪兽')" completionHandler:nil];
}
//两个参数
- (IBAction)twoParameter:(id)sender {
    /*
     *alertSendMsg('我是参数1','我是参数2')
     *alertSendMsg JS方法名
     *我是参数1 带的参数
     *我是参数2
     */
    [self.webView evaluateJavaScript:@"alertSendMsg('我是参数1','我是参数2')" completionHandler:nil];
}

- (void)showMsg:(NSString *)msg {
    [[[UIAlertView alloc] initWithTitle:nil message:msg delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
}

解决POST请求传参不管用问题

当我们用UIWebView POST请求传参时一般是这样写的

// 创建WebView
    UIWebView *webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 设置访问的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]];
    // WebView加载请求
    [webView loadRequest:request];
    // 将WebView添加到视图
    [self.view addSubview:webView];

但是当我们用WKWebView这样加载的时候并没有什么卵用。

解决方法

参考文章: http://stackoverflow.com/questions/26253133/cant-set-headers-on-my-wkwebview-post-request http://www.jianshu.com/p/403853b63537

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

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

######2、将对应的JavaScript代码通过加载本地网页的形式加载到WKWebView

 // 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];
3、Native调用JavaScript脚本并传入参数来完成POST请求

这里需要用到WKWebView和JS的交互

 // 加载完成的代理方法
 - (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) {

     }];
 }

github地址:https://github.com/jiangjunhui1993/KWWebView-JS-