WKWebView 初窥-JS交互探究
前言
iOS 8.0 后, 苹果推出了WKWebView 旨在替代 UIWebView, 之前一直没有时间来对其进行调研使用, 现在项目中需要替换 UIWebView, 因此将自己对其的了解进行简单的记录.
本文的主要内容有:
- WKWebView 的基本使用
- WKWebView 的代理方法
- WKWebView 与 JS 交互方法
(1) JS 调用 OC 方法
(2) OC 调用 JS 方法 - WKWebView 注入 JS 到第三方web页面中
- 循环引用问题
一 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