OC与JS交互之WKWebView

一、WKWebView介绍

WKWebView是Apple在iOS8推出的Webkit框架中的负责网页的渲染与展示的类,相比UIWebView速度更快,占用内存更少,支持更多的HTML特性。

(1)常用属性及方法

// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// UI代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 页面标题, 一般使用KVO动态获取
@property (nullable, nonatomic, readonly, copy) NSString *title;
// 页面加载进度, 一般使用KVO动态获取
@property (nonatomic, readonly) double estimatedProgress;
// 可返回的页面列表, 已打开过的网页, 有点类似于navigationController的viewControllers属性
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;
// 页面url
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
// 页面是否在加载中
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
// 是否可返回
@property (nonatomic, readonly) BOOL canGoBack;
// 是否可向前
@property (nonatomic, readonly) BOOL canGoForward;
// WKWebView继承自UIView, 所以如果想设置scrollView的一些属性, 需要对此属性进行配置
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
//自定义UserAgent, 会覆盖默认的值 ,iOS 9之后有效
@property (nullable, nonatomic, copy) NSString *customUserAgent


// 带配置信息的初始化方法
// configuration 配置信息
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
// 加载请求
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 返回上一级
- (nullable WKNavigation *)goBack;
// 前进下一级, 需要曾经打开过, 才能前进
- (nullable WKNavigation *)goForward;
// 刷新页面
- (nullable WKNavigation *)reload;
// 根据缓存有效期来刷新页面
- (nullable WKNavigation *)reloadFromOrigin;
// 停止加载页面
- (void)stopLoading;
// 执行JavaScript代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

(2)WKWebViewConfiguration

// 通过此属性来执行JavaScript代码来修改页面的行为
@property (nonatomic, strong) WKUserContentController *userContentController;
//***********下面属性一般不需要设置
// 首选项设置,  
//可设置最小字号, 是否允许执行js
//是否通过js自动打开新的窗口
@property (nonatomic, strong) WKPreferences *preferences;
// 是否允许播放媒体文件
@property (nonatomic) BOOL allowsAirPlayForMediaPlayback
// 需要用户来操作才能播放的多媒体类型
@property (nonatomic) WKAudiovisualMediaTypes mediaTypesRequiringUserActionForPlayback
// 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
@property (nonatomic) BOOL allowsInlineMediaPlayback;

(3)WKUserContentController

WKUserContentController 是JavaScript与原生进行交互的桥梁, 主要使用的方法有:

// 注入JavaScript与原生交互协议
// JS 端可通过 window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 发送消息
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
// 移除注入的协议, 在deinit方法中调用
- (void)removeScriptMessageHandlerForName:(NSString *)name;
// 通过WKUserScript注入需要执行的JavaScript代码
- (void)addUserScript:(WKUserScript *)userScript;
// 移除所有注入的JavaScript代码
- (void)removeAllUserScripts;

二、WKWebView中的三个代理方法 

(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

创建一个新的WKWebView

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

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

// 界面弹出警告框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(void (^)())completionHandler;
// 界面弹出确认框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
// 界面弹出输入框
- (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对象。使用WKUserContentController注入的交互协议, 需要遵循WKScriptMessageHandler协议, 在其协议方法中获取JavaScript端传递的事件和参数:

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

WKScriptMessage包含了传递的协议名称及参数, 主要从下面的属性中获取:

// 协议名称, 即上面的add方法传递的name
@property (nonatomic, readonly, copy) NSString *name;
// 传递的参数
@property (nonatomic, readonly, copy) id body;

三、WKWebView-协议拦截

(1)JS调OC

JS代码:

        var loginBtn = document.getElementById("loginID");
        loginBtn.onclick = function () {
            var token = "js_tokenString";
            loginSuccess(token);
        }
        //登录成功
        function loginSuccess(token) {
            var action = "loginSuccess";
            jsToOc(action, token);
        }

        //JS调用OC入口
        function jsToOc(action, params) {
            var url = "jsToOc://" + action + "?" + params;
            window.location.href = url;
        }        

OC代码:

//WKWeView在每次加载请求前会调用此方法来确认是否进行请求跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    
    NSString *ss = navigationAction.request.URL.scheme;
    if ([navigationAction.request.URL.scheme caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) {
        decisionHandler(WKNavigationActionPolicyCancel);
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:navigationAction.request.URL.host message:navigationAction.request.URL.query delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
        
    }else{
         decisionHandler(WKNavigationActionPolicyAllow);
    }
}

//! WKWebView在每次加载请求完成后会调用此方法
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    
    [webView evaluateJavaScript:@"document.title" completionHandler:^(NSString *title, NSError *error) {
        self.title = title;
    }];
}

(2)OC调JS

JS代码:

        function ocToJs(action, params) {
            document.getElementById("returnValue").innerHTML = action + '?' + params;
        }    

OC代码:

#pragma mark - Action
- (void)loginBtn01Action:(UIButton *)btn{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.wkWebView evaluateJavaScript:@"ocToJs('loginSucceed', 'oc_tokenString')" completionHandler:^(id response, NSError *error) {}];
    });
}

四、WKWebView-WKScriptMessageHandler协议

WKScriptMessageHandler是WebKit提供的一种在WKWebView上进行JS消息控制的协议。

(1)JS调OC

实现原理:
1、JS与iOS约定好jsToOc方法,用作JS在调用iOS时的方法;
2、iOS使用WKUserContentController的-addScriptMessageHandler:name:方法监听name为jsToOc的消息;
3、JS通过window.webkit.messageHandlers.jsToOc.postMessage()的方式对jsToOc方法发送消息;
4、iOS在-userContentController:didReceiveScriptMessage:方法中读取name为jsToOc的消息数据message.body。

[userContentController addScriptMessageHandler:self name:@"jsToOc"]会引起循环引用问题。一般来说,在合适的时机removeScriptMessageHandler可以解决此问题。比如:在-viewWillAppear:方法中执行add操作,在-viewWillDisappear:方法中执行remove操作。如下:

[_webView.configuration.userContentController removeScriptMessageHandlerForName:@"jsToOc"];

JS代码:

        var loginBtn = document.getElementById("loginID");
        loginBtn.onclick = function () {
            var token = "js_tokenString";
            loginSuccess(token);
        }
        //登录成功
        function loginSuccess(token) {
            var action = "loginSuccess";
            //jsToOc(action, token);
            window.webkit.messageHandlers.jsToOc.postMessage(action);
        }

OC代码:

WKUserContentController *userContentController = [[WKUserContentController alloc] init];
    [userContentController addScriptMessageHandler:self name:@"jsToOc"];
    
    //! 使用添加了ScriptMessageHandler的userContentController配置configuration
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = userContentController;
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.preferences.minimumFontSize = 18;
    config.userContentController = userContentController;
#pragma mark - WKScriptMessageHandler
//! WKWebView收到ScriptMessage时回调此方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
{
    if ([message.name caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) {
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:message.name message:message.body delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
        NSLog(@"%@", message.body);
    }else if ([message.name caseInsensitiveCompare:@"showMobile"] == NSOrderedSame){
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:message.name message:message.body delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
    }
}

五、WKWebView-WKUIDelegate协议

因为WKWebView的特性。JS在调用alert()、confirm()和prompt()方法时没有反应。若要正常使用这三个方法,iOS需要实现WKUIDelegate中的三个方法模拟JS的这三个方法。

JS调OC

JS代码:

        //! 调用alert(message)
        function showAlert() {
            alert("js_alertMessage");
        }
    
        //! 调用confirm(message)
        function showConfirm() {
            confirm("js_confirmMessage");
        }
    
        //! 调用prompt(prompt, defaultMessage)
        function showPrompt() {
            prompt("js_prompt", "js_prompt_defaultMessage");
        }

OC代码:

#pragma mark - WKUIDelegate
//! alert(message)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }];
    [alertController addAction:cancelAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

//! confirm(message)
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Confirm" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }];
    UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }];
    [alertController addAction:cancelAction];
    [alertController addAction:confirmAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

//! prompt(prompt, defaultText)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.placeholder = defaultText;
    }];
    UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text);
    }];
    [alertController addAction:confirmAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

参考:https://www.jianshu.com/p/e23aa25d7514 

     https://www.jianshu.com/p/905b40e609e2

   https://www.cnblogs.com/markstray/p/5757264.html

GitHub:https://github.com/hongsheng1024/RunTime

原文地址:https://www.cnblogs.com/whongs/p/9880289.html