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

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是例代码如下:




    
    


        
姓名:
密码:

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 
#import 

NS_ASSUME_NONNULL_BEGIN

@protocol BFWKDelegate 

-(void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message;

@end

@interface BFWKDelegateController : UIViewController 

@property (weak , nonatomic) id 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 
#import 

@protocol PersonJSExport 

@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交互(一))