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