WKWebView 初窥-JS交互探究 前言 一 WKWebView 与 UIWebView 的区别: 二 WKWebView的简单使用 三 WKWebView 与 JS 交互 四 WKWebView 注入自定义js到webView 五 在实现 WKScriptMessageHandler, 很容易引起循环引用的问题. 引用关系:

iOS 8.0 后, 苹果推出了WKWebView 旨在替代 UIWebView, 之前一直没有时间来对其进行调研使用, 现在项目中需要替换 UIWebView, 因此将自己对其的了解进行简单的记录.
本文的主要内容有:

  1. WKWebView 的基本使用
  2. WKWebView 的代理方法
  3. WKWebView 与 JS 交互方法
    (1) JS 调用 OC 方法
    (2) OC 调用 JS 方法
  4. WKWebView 注入 JS 到第三方web页面中
  5. 循环引用问题

一 WKWebView 与 UIWebView 的区别:

(1) 在性能、稳定性、功能方面有很大提升;
(2) 允许JavaScript的Nitro库加载并使用(UIWebView中限制);
(3) 支持了更多的HTML5特性;
(4) 高达60fps的滚动刷新率以及内置手势;
(5) 将UIWebViewDelegate与UIWebView重构成了14类与3个协议(苹果官方文档
在 viewDidLoad 中用同样的方式创建 UIWebView 和 WKWebView 加载 百度 时内存对比如下:
UIWebView( 54.39 M):
WKWebView 初窥-JS交互探究
前言
一 WKWebView 与 UIWebView 的区别:
二 WKWebView的简单使用
三 WKWebView 与 JS 交互
四 WKWebView 注入自定义js到webView
五 在实现 WKScriptMessageHandler, 很容易引起循环引用的问题. 引用关系:
WKWebView ( 24.16M):
WKWebView 初窥-JS交互探究
前言
一 WKWebView 与 UIWebView 的区别:
二 WKWebView的简单使用
三 WKWebView 与 JS 交互
四 WKWebView 注入自定义js到webView
五 在实现 WKScriptMessageHandler, 很容易引起循环引用的问题. 引用关系:

二 WKWebView的简单使用

1. 初始化方法

类似于UIWebView, WKWebView (需要引用头文件#import

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

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

2. 加载网页的方式(与UIWebView类似)

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
[self.view addSubview:webView];

3. 代理方法

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

/* 1.在发送请求之前,决定是否跳转  */
- (void)webView:(WKWebView *)webView 
        decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 
                        decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

/* 2.页面开始加载 */
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;

/* 3.在收到服务器的响应头,根据response相关信息,决定是否跳转。 */
- (void)webView:(WKWebView *)webView 
        decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse 
                          decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

/* 4.开始获取到网页内容时返回 */
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;

/* 5.页面加载完成之后调用 */
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;

/* error - 页面加载失败时调用, 提交 navigation错误的时候调用 */
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;

/* error - 页面加载失败时调用 */
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;

/* 其他 - 处理服务器重定向Redirect */
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;

/* 其他 - 网页加载内容进程终止, 用于处理白屏问题 */
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;

其中, 如果实现了代理方法:

/* 1.在发送请求之前,决定是否跳转  */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    /* 必须调用 否则崩溃 允许跳转, 类似于 webView:shouldStartLoadWithRequest 返回 YES */
    decisionHandler(WKNavigationActionPolicyAllow);
}

/* 3.在收到服务器的响应头,根据response相关信息,决定是否跳转。 */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
    /* 必须调用 否则崩溃, 允许跳转, 类似于 webView:shouldStartLoadWithRequest 返回 YES */
    decisionHandler(WKNavigationResponsePolicyAllow);
}

(2) WKUIDelegate

// 创建一个新的WebView
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

剩下三个代理方法全都是与界面弹出提示框相关的,针对于web界面的三种提示框(警告框、确认框、输入框)分别对应三种代理方法。

/* 输入框,页面中有调用JS的 prompt 方法就会调用该方法 */
- (void)webView:(WKWebView *)webView 
        runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler;

/* 确认框,页面中有调用JS的 confirm 方法就会调用该方法 */
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

/* 警告框,页面中有调用JS的 alert 方法就会调用该方法 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

(3) WKScriptMessageHandler

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

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

三 WKWebView 与 JS 交互

1. JS 调用 Native

WKWebView 和 js 交互通过js代码中的

window.webkit.messageHandlers.<对象名>.postMessage(<数据>)

获取对象名和数据, 然后通过配置,设置与web对应的JS方法名称,通过配置之后可以在代理中进行调用对应的web的JS方法.

// 进行配置控制器
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    // 实例化对象
    configuration.userContentController = [[WKUserContentController alloc] init];
    /* 调用js方法, js方法定义中的字段 window.webkit.messageHandlers.<对象名>.postMessage(<回传数据>); */
    [configuration.userContentController addScriptMessageHandler:self name:@"对象名"];
    /*最后通过定制的 configuration 初始化WKWebView*/
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
   /*webView加载web界面(其中含有自己的js)*/
    /*....*/
    /*
    如果JS中有Alert, confirm 或者 prompt, 需要实现 WKUIDelegate的相关方法
     webView.UIDelegate = self;
*/
  [self.view addSubview:webView];

然后实现WKScriptMessageHandler的代理方法:

#pragma mark -- WKScriptMessageHandlerDelegate
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@,  body:%@", message.name, message.body);
    if ([message.name isEqualToString:@"对象名"]) {
       /*具体需要调用的OC方法*/
        NSLog(@"调用OC方法成功");
    } 
}

这样就可以简单的完成一次JS调用OC的方法了, 但是如果JS中用 alert , confirm 或者 prompt 时, 我们发现相关的提示界面不会出现, 这时需要实现 WKUIDelegate 的相关方法.

#pragma mark -- WKUIDelegate
// 提醒 对应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{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }]];

    [self presentViewController:alert animated:YES completion:nil];
}

// 确认提交 对应js的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    // 按钮
    UIAlertAction *alertActionCancel = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        // 返回用户选择的信息
        completionHandler(NO);
    }];
    UIAlertAction *alertActionOK = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }];
    // alert弹出框
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:alertActionCancel];
    [alertController addAction:alertActionOK];
    [self presentViewController:alertController animated:YES completion:nil];

}

// 文本框输入 对应js的prompt方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    // alert弹出框
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert];
    // 输入框
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.placeholder = defaultText;
    }];
    // 确定按钮
    [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        // 返回用户输入的信息
        UITextField *textField = alertController.textFields.firstObject;
        completionHandler(textField.text);
    }]];
    // 显示
    [self presentViewController:alertController animated:YES completion:nil];

}

2. OC 调用 JS 方法

在 WKWebView 中, 移除了方法

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

调用OC方法改成了

/*javaScriptString:所执行的JS代码
completionHandler:执行结束后的回调
*/
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

四 WKWebView 注入自定义js到webView

需求: WKWebView 往 web 页面中注入自定义的 js, 并实现用自定义的js回调自己 OC 的方法.
场景: 当我们加载第三方web页面的时候, 可以成功的显示, 但是我们想往这个web页面中注入我们自己的一段js函数, 并在其运行结束后, 调用相关的OC方法, 这时我们不知道第三方的js, 没有办法直接用addScriptMessageHandler:name: 添加相关的函数名称, 这时应该怎么办呢?
我们可以在创建 WKWebView 的时候, 往第三方web页面中注入js, 当点击某个按钮时, 运行注入的js函数, 并获取回传值, 具体做法如下:

// 进行配置控制器
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    // 实例化对象
    configuration.userContentController = [[WKUserContentController alloc] init];
    // 注入一段自定义的js代码到网页中
    NSString *jsStr = @"function myfunction(){var x=5+1; alert(x); window.webkit.messageHandlers.hello.postMessage(x);}";
    // WKUserScriptInjectionTimeAtDocumentEnd为网页加载完成时注入
    WKUserScript *script = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    [configuration.userContentController addUserScript:script];
    [configuration.userContentController addScriptMessageHandler:self name:@"hello"];

    _webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

 /*WKWebView加载第三方页面*/
/*如果js含有alert等, 实现WKUIDelegate代理*/
_webView.UIDelegate = self;

    [self.view addSubview:_webView];

如果注入的js含有alert等, 要实现 WKUIDelegate 的代理方法, 方法同上.
点击某个按钮(或者某个时间节点)执行这个js函数

- (void)btnClick{
    // 执行注入的js代码
    [_webView evaluateJavaScript:@"myfunction()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"result=%@, Error=%@", result, error);
    }];
}

并实现WKScriptMessageHandler代理方法

#pragma mark -- WKScriptMessageHandlerDelegate
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"body:%@",message.body);
    if ([message.name isEqualToString:@"hello"]){
         /*调用的OC方法*/
        NSLog(@"自定义js调用oc方法成功");
    }
}

五 在实现 WKScriptMessageHandler, 很容易引起循环引用的问题. 引用关系:

self-->webView-->configuration-->userContentControll-->self 

一般有两种解决办法:
(1) 在视图即将消失的时候, 移除所有的MessageHandler

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    // 视图即将消失的时候, 移除 防止循环引用
    [_webView.configuration.userContentController removeAllUserScripts];
    // self-->webView-->configuration-->userContentControll-->self 循环引用
}

注意: 不是在dealloc中移除, 因为已经循环引用了, 不会执行dealloc方法.
(2) 思路是: 另外创建一个代理对象,然后通过代理对象回调指定的self.(因为没有尝试, 不敢乱说, 还是参考其他大佬的吧)
具体参考: 使用WKWebView替换UIWebView

完整Demo见: [ github Demo ], 喜欢的希望可以star下, 非常感谢.

上述仅是个人见解, 如有错误欢迎指出, 谢谢.

参考内容:
IOS进阶之WKWebView
WKWebView的新特性与使用
iOS WKWebView与JS交互
js与OC交互(WKWebView)
iOS学习笔记14-网络(三)WebView