美文网首页iOS底层
WKWebView进阶使用 - JS交互(一)

WKWebView进阶使用 - JS交互(一)

作者: smile_frank | 来源:发表于2022-02-10 11:36 被阅读0次

    WKWebView基础

    WKWebView的优势

    1、更多的支持HTML5的特性
    2、官方宣称的高达60fps的滚动刷新率以及内置手势
    3、将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。官方文档说明
    4、Safari相同的JavaScript引擎
    5、占用更少的内存
    6、增加加载进度属性:estimatedProgress

    基本使用方法

    WKWebView有两个代理delegate,WKUIDelegateWKNavigationDelegate

    WKNavigationDelegate

    WKNavigationDelegate主要处理一些跳转、加载处理操作

    WKUIDelegate

    WKUIDelegate主要处理JS脚本,确认框,警告框等。

    - (void)viewDidLoad {
        [super viewDidLoad];
        webView = [[WKWebView alloc]init];
        [self.view addSubview:webView];
        [webView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.view);
            make.right.equalTo(self.view);
            make.top.equalTo(self.view);
            make.bottom.equalTo(self.view);
        }];
        webView.UIDelegate = self;
        webView.navigationDelegate = self;
        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
    }
    

    加载本地的HTML文件

     /*
         参数1:index 是要打开的html的名称
         参数2:html 是index的后缀名
         参数3:HtmlFile/app/index 是文件夹的路径
     */
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html" inDirectory:@"html"];
    NSURL *pathURL = [NSURL fileURLWithPath:filePath];
    [_webView loadRequest:[NSURLRequest requestWithURL:pathURL]];  
    
    #pragma mark- WKNavigationDelegate
    /*
     WKNavigationDelegate主要处理一些跳转、加载处理操作
     */
    
    //页面开始加载时调用
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
        NSLog(@"----页面开始加载");
    }
    
    //当内容开始返回时调用
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
        NSLog(@"----页面返回内容");
    }
    
    //页面加载完成时调用
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
        NSLog(@"----页面加载完成");
    }
    
    //页面加载失败时调用
    - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
        NSLog(@"----页面加载失败");
    }
    
    //接收到服务器跳转请求之后调用
    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
        
        NSLog(@"----接收到服务器跳转请求之后调用");
    }
    
    //在收到响应后,决定是否跳转
    -(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
         NSLog(@"%@",navigationResponse.response.URL.absoluteString);
        //允许跳转
        decisionHandler(WKNavigationResponsePolicyAllow);
        //不允许跳转
        //decisionHandler(WKNavigationResponsePolicyCancel);
    }
    
    
    //在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        NSLog(@"%@",navigationAction.request.URL.absoluteString);
        //允许跳转
        decisionHandler(WKNavigationActionPolicyAllow);
        //不允许跳转
        //decisionHandler(WKNavigationActionPolicyCancel);
    }
    
    #pragma mark- WKUIDelegate
    
    // WKUIDelegate是web界面中有弹出警告框时调用这个代理方法,主要是用来处理使用系统的弹框来替换JS中的一些弹框的,比如: 警告框, 选择框, 输入框等
    /**
     webView中弹出警告框时调用, 只能有一个按钮
     @param webView webView
     @param message 提示信息
     @param frame 可用于区分哪个窗口调用的
     @param completionHandler 警告框消失的时候调用, 回调给JS
     */
    
    - (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:nil]];
        [self presentViewController:alert animated:YES completion:nil];
        completionHandler();
        
    }
    
    /** 对应js的confirm方法
     webView中弹出选择框时调用, 两个按钮
     @param webView webView description
     @param message 提示信息
     @param frame 可用于区分哪个窗口调用的
     @param completionHandler 确认框消失的时候调用, 回调给JS, 参数为选择结果: YES or NO
     */
    
    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
      
    }
    
    // JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
    
    /** 对应js的prompt方法
     webView中弹出输入框时调用, 两个按钮 和 一个输入框
     
     @param webView webView description
     @param prompt 提示信息
     @param defaultText 默认提示文本
     @param frame 可用于区分哪个窗口调用的
     @param completionHandler 输入框消失的时候调用, 回调给JS, 参数为输入的内容
     */
    
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
       
           UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"请输入" message:prompt preferredStyle:(UIAlertControllerStyleAlert)];
           [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
           textField.placeholder = @"请输入";
           }];
           UIAlertAction *ok = [UIAlertAction actionWithTitle:@"确定" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) {
           UITextField *tf = [alert.textFields firstObject];
           completionHandler(tf.text);
               
           }];
           UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:^(UIAlertAction * _Nonnull action) {
           completionHandler(defaultText);
           }];
           [alert addAction:ok];
           [alert addAction:cancel];
           [self presentViewController:alert animated:YES completion:nil];
    }
    
    

    OC与JS交互

    JS 调取OC

    方案1: WKUserContentController注册方法监听

    js是例代码如下:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
            <table width="80%" align="center" border="0">
                <tr>
                    <td width="30%" align="right">姓名:</td>
                    <td align="left">
                        <input type="text" name="username">
                    </td>
                </tr>
                <tr>
                    <td width="30%" align="right">密码:</td>
                    <td align="left">
                        <input type="text" name="password" >
                    </td>
                </tr>
                <tr >
                     <td align="right">
                        <input type="button" name="submit" value="提交" onclick="jsCallNativeMethod()">
                    </td>
                     <td align="left">
                        <input type="button" name="login" value="登录" onclick="bf_jsCallNativeMethod()">
                    </td>
                    <td></td>
               </tr>
            </table>
            <script type="text/javascript">
                function  jsCallNativeMethod(){
                    location.href = "js_native://alert";
                    
                }
                function nativeCallbackJscMothod(arguments) {
                    alert('原生调用js方法 传来的参数 = ' + arguments);
                }   
                function bf_jsCallNativeMethod() {
                    window.webkit.messageHandlers.bf_jsCallNativeMethod.postMessage('我的参数222');
                }     
                function add(a , b) {
                    return a + b
                }           
            </script>
    </body>
    </html>
    
    

    OC代码如下:

    #pragma mark- 懒加载
    /*
    * WKWebViewConfiguration用来初始化WKWebView的配置。
    * WKPreferences配置webView能否使用JS或者其他插件等
    * WKUserContentController用来配置JS交互的代码
    * UIDelegate用来控制WKWebView中一些弹窗的显示(alert、confirm、prompt)。
    * WKNavigationDelegate用来监听网页的加载情况,包括是否允许加载,加载失败、成功加载等一些列代理方法。
    */
    - (WKWebView *)webView {
        if (!_webView) {
            //网页配置文件
            WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
            //允许与网页交互
            configuration.selectionGranularity = YES;
        
            WKPreferences *preferences = [WKPreferences new];
            preferences.javaScriptCanOpenWindowsAutomatically = YES;
            preferences.minimumFontSize = 50;
            configuration.preferences = preferences;
            
            //注册方法
            WKUserContentController *userContentController = [[WKUserContentController alloc]init];
            //注册一个name为bf_jsCallNativeMethod的js方法(要记的remove)
            [userContentController addScriptMessageHandler:self name:@"bf_jsCallNativeMethod"];
            
            configuration.userContentController = userContentController;
            
            _webView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:configuration];
            _webView.navigationDelegate = self;
            _webView.UIDelegate = self;
        }
        return _webView;
    }
    
    #pragma mark - WKScriptMessageHandler   
    
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
        if ([message.name isEqualToString:@"bf_jsCallNativeMethod"]) {
            NSLog(@"这个是传过来的参数%@",message.body);     
            [self.webView evaluateJavaScript:@"nativeCallbackJscMothod('6666')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
                NSLog(@"执行完的x==%@,error = %@",x,error.localizedDescription);
            }];
            
        }
    }
    
    /*要记得销毁是移除*/
    - (void)dealloc
    {
        NSLog(@"--delloc--");
        //这里需要注意,前面增加过的方法一定要remove掉
        [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"bf_jsCallNativeMethod"];
    }
    

    上面的OC代码如果认证测试一下就会发现dealloc并不会执行,这样肯定是不行的,会造成内存泄漏。原因是[userContentController addScriptMessageHandler:self name:@"bf_jsCallNativeMethod"];这句代码造成无法释放内存。(PS:我也想到NSTimer中遇到的循环引用问题,之前总结的文章使用Weak指针还是不能释放)

    改进方案

    用一个新的controller来处理,新的controller再绕用delegate绕回来。

    BFWKDelegateController

    #import <UIKit/UIKit.h>
    #import <WebKit/WebKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @protocol BFWKDelegate <NSObject>
    
    -(void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message;
    
    @end
    
    @interface BFWKDelegateController : UIViewController <WKScriptMessageHandler>
    
    @property (weak , nonatomic) id<BFWKDelegate> delegate;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    #import "BFWKDelegateController.h"
    
    @interface BFWKDelegateController ()
    @end
    
    @implementation BFWKDelegateController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
      
    }
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([self.delegate respondsToSelector:@selector(bf_userContentController:didReceiveScriptMessage:)]) {
            [self.delegate bf_userContentController:userContentController didReceiveScriptMessage:message];
        }
    }
    @end
    
    
    
    
    - (WKWebView *)webView {
        if (!_webView) {
            //网页配置文件
            WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
            //允许与网页交互
            configuration.selectionGranularity = YES;
    
            WKPreferences *preferences = [WKPreferences new];
            preferences.javaScriptCanOpenWindowsAutomatically = YES;
            preferences.minimumFontSize = 50;
            configuration.preferences = preferences;
            //中间的Delegate
            BFWKDelegateController *bfDelegateController = [[BFWKDelegateController alloc]init];
            bfDelegateController.delegate = self;
            
            //注册方法
            WKUserContentController *userContentController = [[WKUserContentController alloc]init];
            //注册一个name为bf_jsCallNativeMethod的js方法
            [userContentController addScriptMessageHandler:bfDelegateController name:@"bf_jsCallNativeMethod"];
    
            configuration.userContentController = userContentController;
    
            _webView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:configuration];
            _webView.navigationDelegate = self;
            _webView.UIDelegate = self;
        }
        return _webView;
    }
    
    /*要记得销毁是移除*/
    - (void)dealloc
    {
        NSLog(@"--delloc--");
        //这里需要注意,前面增加过的方法一定要remove掉
        [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"bf_jsCallNativeMethod"];
    }
    
    
    #pragma mark - 中转的Delegate
    - (void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
       
        NSLog(@"name:%@ body:%@ frameInfo:%@",message.name,message.body,message.frameInfo);
    }
    
    

    在运行一下,当前页面销毁的时候可以执行到dealloc代码啦

    注意点:
    1、addScriptMessageHandler要和removeScriptMessageHandlerForName配套出现,否则会造成内存泄漏。
    2、h5只能传一个参数,如果需要多个参数就需要用字典或者json组装。

    方案2:通过拦截WKNavigationDelegate代理

    通过拦截WKNavigationDelegate的代理方法,然后匹配目标字符串

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(**void** (^)(WKNavigationActionPolicy))decisionHandler;
    

    具体实例代码如下:通过匹配是否是以js_native://alert开头的字符串

    //在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        
        NSURL *url = navigationAction.request.URL;
        if ([[url absoluteString] hasSuffix:@"js_native://alert"]) {
            [self handleJSMessage];
            //不允许跳转
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        //允许跳转
        decisionHandler(WKNavigationActionPolicyAllow);
        
    }
    
    #pragma mark - Private
    
    -(void)handleJSMessage {
        [_webView evaluateJavaScript:@"nativeCallbackJscMothod('123')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
            NSLog(@"x = %@, error = %@", x, error.localizedDescription);
        }];
    }
    
    

    方案3:苹果原生API:JavaScriptCore (iOS7.0+ 使用)

    主要的两个类:

    • JSContext : JS上下文(运行环境),可用对象方法去执行JS代码(evaluateScript),可通过上下文对象去获取JS里的数据(上下文对象[key]),并用JSValue对象接收
    • JSValue : 用于接收JSContext对象获取的数据,可以是任意对象,方法。

    实例代码如下:

        JSContext *jsContent = [[JSContext alloc]init];
        jsContent[@"add"] = ^(int a, int b){
            NSLog(@"a+b = %d",a + b);
        };
        [jsContent evaluateScript:@"add(20,30)"];
    

    注意:js代码要先被执行,才能通过上下文获取

    说明:JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为JSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。
    所以,在用block时,需考虑循环引用问题

    • 不要在Block中直接使用JSValue:建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了
    • 不要在Block中直接使用JSContext:可以使用[JSContext currentContext] 方法来获取当前的Context
      1、block方式:

    用block定义js函数,并执行(OC调用执行js,而js是调用的oc block)

    - (void)jsCallOCBlock
    {
    
    JSContext *ctx = [[JSContext alloc] init];
    
    //OC中NSBlock对应js中Function object
    ctx[@"goto"] = ^(NSString *parmStr){
    
       //block内不要直接使用ctx,会循环引用(ctx已经引用block),若外部有JSValue,也不能在block内直接调用(JSValue强持有了ctx )
    
        //获取JS调用参数
        NSLog(@"parmStr:%@",parmStr);
    
        //可以直接获取所有参数
        NSArray *arguments = [JSContext currentArguments];
        NSLog(@"%@",arguments[0]);
    };
    
    //JS执行代码,调用goto方法,并传入参数school
    NSString *jsCode = @"goto('school')";
    
    //执行
    [ctx evaluateScript:jsCode];
    }
    

    2、JSExport 协议
    需要在JS中生成OC对应的类,然后再通过JS调用。

    方法:
    通过自定义一个遵循JSExport的协议,把需要被JS访问的OC类中的属性,方法暴露给JS使用

    步骤:
    1、自定义协议,协议遵循JSExport的协议,协议中的属性和方法就是OC对象要暴露给JS,让JS可以直接调用

    #import <Foundation/Foundation.h>
    #import <JavaScriptCore/JavaScriptCore.h>
    
    @protocol PersonJSExport <JSExport>
    
    @property (nonatomic, strong) NSString *address;
    
    //无参数方法
    - (void)play;
    
    //因为JS函数命名规则和OC规则不一样,所以当有多个参数时,可以使用OC提供了一个宏*JSExportAs*,指定JS应该生成什么样的函数来对应OC的方法。
    //不使用JSExportAs指定关联也可以正常调用,后面直接接多个参数
    
    //多参数方法
    
    //不指定关联
    - (void)play:(NSString *)address time:(NSString *)time;
    //指定关联
    JSExportAs(gotoSchool, - (void)goToSchoolWithSchoolName:(NSString *)name address:(NSString *)address);
    
    @end
    

    2、自定义一个遵循第一步创建的协议的OC对象,实现协议的方法

    例如:Person对象

    3、JS调用对象协议声明的方法

    - (void)jsCallOCClass
    {
    // 创建Person对象,Person对象必须遵守JSExport协议
    Person *p = [[Person alloc] init];
    p.address = @"aaa";
    
    JSContext *ctx = [[JSContext alloc] init];
    // 在JS中生成Person对象person,并且拥有p内部的值
    ctx[@"person"] = p;
    
    // 执行JS代码
    //NSString *jsCode = @"person.play()";
    NSString *jsCode = @"person.play('北京天安门','now')";
    //NSString *jsCode = @"person.gotoSchool('实验中学','广州')";
    
    [ctx evaluateScript:jsCode];
    }
    

    另外,若要调用OC系统的类,例如UIView
    需要同样创建协议,只是在第三步用runtime给系统类UIView添加创建的协议
    class_addProtocol([UIView class], @protocol(UIViewlJSExport));

    方案4: 三方库 WebViewJavaScripteBridge

    OC 调取JS

    方案1: WKWebView直接执行 evaluateJavaScript 方法

    //原生回调JS
    [self.webView evaluateJavaScript:@"nativeCallbackJscMothod('%@')" completionHandler:^(**id** **_Nullable** s, NSError * **_Nullable** error) {
          NSLog(@"执行完成啦");
    }];
    

    方案2:苹果原生API:JavaScriptCore (iOS7.0+ 使用)

    oc获取js变量,注:js代码要先被执行,才能通过上下文获取

    - (void)ocGetJSVar
    {
    //定义JS代码
    NSString *jsCode = @"var a = 'a'";
    
    //创建JS运行环境
    JSContext *ctx = [[JSContext alloc] init];
    
    //!!!:执行JS代码---先执行,后面才能获取
    [ctx evaluateScript:jsCode];
    
    //获取变量
    JSValue *value = ctx[**@“a"**];
    
    //JSValue转NSString
    NSString *valueStr = value.toString;
    
    //打印结果:a
    NSLog(@"%@",valueStr);
    }
    

    oc调用js方法,并获取返回结果

    - (void)ocCallJSFunc
    {
    NSString *jsCode = @"function say(str){"
    " return str; "
    "}";
    
    // 创建JS运行环境
    JSContext *ctx = [[JSContext alloc] init];
    
    // 执行JS代码
    [ctx evaluateScript:jsCode];
    
    //!!!:执行JS代码---先执行,后面才能获取
    JSValue *say = ctx[@"say"];
    
    // OC调用JS方法,获取方法返回值
    JSValue *result = [say callWithArguments:@[@"hello world!"]];
    
    // 打印结果:hello world!
    NSLog(@"%@",result);
    }
    

    方案3:三方库 WebViewJavaScripteBridge

    参考:
    JavaScriptCore深入浅出
    OC和JS调用

    相关文章

      网友评论

        本文标题:WKWebView进阶使用 - JS交互(一)

        本文链接:https://www.haomeiwen.com/subject/hwenkrtx.html