zoukankan      html  css  js  c++  java
  • WKWebView 初窥-JS交互探究

    前言

    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 ( 24.16M):
    这里写图片描述

    二 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

  • 相关阅读:
    安装Hadoop单机版
    Linux的en33没有IP地址
    idea导入spring源码
    CSS 选择器之基础选择器
    CSS 简介和代码风格
    JavaScript 中 双感叹号 !! 的作用
    VS Code 中的settings.js 配置
    项目中使用 vuex 实现(状态)数据共享
    VS Code 相关设置
    Node.js 的简单了解
  • 原文地址:https://www.cnblogs.com/xiaocai-ios/p/7779745.html
Copyright © 2011-2022 走看看