WKWebView自出现以来一直被人们所推崇,原因是他的优点很多:
更多的支持HTML5的特性,与JS交互更容易
官方宣称的高达60fps的滚动刷新率以及内置手势
将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。
Safari相同的JavaScript引擎
占用更少的内存,加载速度快
但是我一直以来对WKWebView都不是太熟悉,所以最近抽时间学习了一下,并做了总结
WKWebView有两个delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等。因此WKNavigationDelegate更加常用。
WKWebview提供了API实现js交互 不需要借助JavaScriptCore或者webJavaScriptBridge(由于WKWebView是在一个单独的进程中运行,我们无法获取到 JSContext,所以我们无法使用 JSCore 这个强大的框架来进行交互)。使用WKWebViewConfiguration类中的一个属性WKUserContentController,即userContentController,来实现js native交互。简单的说就是先注册约定好的方法,然后再调用。在 WKWebVeiw 中,我们使用我们有两种方式来调用 JS,一种是使用 WKUserScript;另一种是直接调用 JS 字符串
然后列举下里面所包含的类和协议
类
WKBackForwardList 之前访问过的 web 页面的列表,可以通过后退和前进动作来访问到。
WKBackForwardListItem: webview 中后退列表里的某一个网页。
WKFrameInfo: 包含一个网页的布局信息。
WKNavigation: 包含一个网页的加载进度信息。
WKNavigationAction: 包含可能让网页导航变化的信息,用于判断是否做出导航变化。
WKNavigationResponse: 包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。
WKPreferences: 概括一个 webview 的偏好设置。
WKProcessPool: 表示一个 web 内容加载池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含网页发出的信息。
WKUserScript: 表示可以被网页接受的用户脚本。WKWebViewConfiguration: 初始化 webview 的设置。
WKWindowFeatures: 指定加载新网页时的窗口属性。
协议
WKNavigationDelegate: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。
WKScriptMessageHandler: 提供从网页中收消息的回调方法。
WKUIDelegate: 提供用原生控件显示网页的方法回调。
协议方法
#pragma mark - WKNavigationDelegate 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{
}
#pragma mark - WKNavigationDelegate WKNavigtionDelegate来进行页面跳转
// 接收到服务器跳转请求之后调用,接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
}
// 在收到响应后,决定是否跳转。根据客户端受到的服务器响应头以及response相关信息来决定是否可以跳转
- (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{
//获取请求的url路径
NSLog(@"%@",navigationAction.request.URL.absoluteString);
// 遇到要做出改变的字符串
NSString *subStr = @"www.baidu.com";
if ([navigationAction.request.URL.absoluteString rangeOfString:subStr].location != NSNotFound) {
//回调的URL中如果含有百度,就直接返回,也就是关闭了webView界面
[self.navigationController popViewControllerAnimated:YES];
}
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
//不允许跳转
//decisionHandler(WKNavigationActionPolicyCancel);
}
//需要响应身份验证时调用 同样在block中需要传入用户身份凭证
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
//用户身份信息
NSURLCredential * newCred = [[NSURLCredential alloc] initWithUser:@"user123" password:@"123" persistence:NSURLCredentialPersistenceNone];
//为 challenge 的发送方提供 credential
[challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
//进程被终止时调用(当 WKWebView 总体内存占用过大,页面即将白屏时会调用该方法)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
}
#pragma mark - WKUIDelegate
// 创建一个新的WebView,解决点击内部链接没有反应问题
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
return [[WKWebView alloc]init];
}
//2.WebVeiw关闭(9.0中的新方法)
- (void)webViewDidClose:(WKWebView *)webView{
}
// 弹出一个输入框(与JS交互的)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{
completionHandler(@"http");
}
// 显示一个确认框(JS的)
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
completionHandler(YES);
}
// 显示一个JS的Alert(与JS交互)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
NSLog(@"%@",message);
completionHandler();
}
WKWebView与cookie
这里说下为什么在WKWebView要添加cookie:,其原因是因为WKWebView是在一个单独的进程中运行,所以有时候登录状态会丢失,所以需要cookie.
1.在创建的时候存放到WKUserScript中进行添加cookie
//创建配置
WKWebViewConfiguration *webConfig = [[WKWebViewConfiguration alloc] init];
// 设置偏好设置
webConfig.preferences = [[WKPreferences alloc] init];
// 默认为0
webConfig.preferences.minimumFontSize = 10;
//打开js交互 默认为YES
webConfig.preferences.javaScriptEnabled = YES;
//不通过用户交互,是否可以打开窗口
// 在iOS上默认为NO,表示不能自动通过窗口打开
webConfig.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// web内容处理池
webConfig.processPool = [[WKProcessPool alloc] init];
// 将所有cookie以document.cookie = 'key=value';形式进行拼接
#warning 然而这里的单引号一定要注意是英文的,不要问我为什么告诉你这个(手动微笑)
NSString *cookieValue = @"document.cookie = 'fromapp=ios';document.cookie = 'channel=appstore';";
// 加cookie给h5识别,表明在ios端打开该地址
WKUserContentController* userContentController = WKUserContentController.new;
//下面一段也是原生吊JS的方法
// source 就是我们要调用的 JS 函数
// injectionTime 这个参数我们需要指定一个时间,在什么时候把我们在这段 JS 注入到 WebVeiw 中,它是一个枚举值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
// MainFrameOnly 因为在 JS 中,一个页面可能有多个 frame,这个参数指定我们的 JS 代码是否只在 mainFrame 中生效
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: cookieValue
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
webConfig.userContentController = userContentController;
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:frame configuration:webConfig];
//获取web的标题
[webview addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
//设置导航代理
_webView.navigationDelegate = self;
//[UIColor clearColor]
_webView.backgroundColor = [UIColor orangeColor];
//打开网页间的 滑动返回
_webView.allowsBackForwardNavigationGestures =YES;
//滑动减速的速度
_webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
//禁止滚动
//_webView.scrollView.scrollEnabled = NO;
//弹簧效果
//_webView.scrollView.bounces = YES;
加载某个url的时候添加cookie
//如果WKWebView在加载url的时候需要添加cookie,需要先手动获取当前NSHTTPCookieStorage中的所有cookie,然后将cookie放到NSMutableURLRequest请求头中
- (void)loadRequestWithUrlString:(NSString *)urlString withWeb:(WKWebView *)web{
// 在此处获取返回的cookie
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
[web loadRequest:request];
}
第二部分、添加进度条
#import <WebKit/WebKit.h>
@property (nonatomic, strong) WKWebView *webview;
@property (nonatomic, strong)UIProgressView *progressView;
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.webview = [[WKWebView alloc]initWithFrame:CGRectMake(0, 64, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height)];
NSString *urlString = @"https://www.baidu.com/";
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
request.timeoutInterval = 15.0f;
[self.webview loadRequest:request];
[self.view addSubview:self.webview];
//进度条初始化
self.progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, 1)];
//设置进度条的高度,下面这句代码表示进度条的宽度变为原来的1倍,高度变为原来的1.5倍.
self.progressView.transform = CGAffineTransformMakeScale(1.0f, 2.0f);
[self.webview addSubview:self.progressView];
// 为进度条KVO
[self.webview addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
//添加监测网页标题title的观察者
[self.webView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
}
// 计算wkWebView进度条
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == self.webview && [keyPath isEqualToString:@"estimatedProgress"]) {
CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
if (newprogress == 1) {
self.progressView.hidden = YES;
[self.progressView setProgress:0 animated:NO];
}else {
self.progressView.hidden = NO;
[self.progressView setProgress:newprogress animated:YES];
}
}else if([keyPath isEqualToString:@"title"]){
self.navigationItem.title = _webView.title;
}
}
// 记得取消监听
- (void)dealloc {
[self.webview removeObserver:self forKeyPath:@"estimatedProgress"];
//移除观察者
//[_webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
//[_webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(title))];
}
如果不想用progressView,那么我们可以用UIView自定义一个progressView
@property (nonatomic, strong)CALayer *progresslayer;
UIView *progress = [[UIView alloc]initWithFrame:CGRectMake(0, 64, CGRectGetWidth(self.view.frame), 3)];
progress.backgroundColor = [UIColor clearColor];
[self.view addSubview:progress];
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, 0, 3);
layer.backgroundColor = [UIColor orangeColor].CGColor;
[progress.layer addSublayer:layer];
self.progresslayer = layer;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"estimatedProgress"]) {
self.progresslayer.opacity = 1;
//不要让进度条倒着走...有时候goback会出现这种情况
if ([change[@"new"] floatValue] < [change[@"old"] floatValue]) {
return;
}
self.progresslayer.frame = CGRectMake(0, 0, self.view.bounds.size.width * [change[@"new"] floatValue], 3);
if ([change[@"new"] floatValue] == 1) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.progresslayer.opacity = 0;
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.progresslayer.frame = CGRectMake(0, 0, 0, 3);
});
}
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
第三部分、原生和 JS 交互
先说 JS 调用原生的方式
1.利用 WKUIDelegate 的三个代理方法对 JS 进行拦截
alert() 弹出个提示框,只能点确认无回调
confirm() 弹出个确认框(确认,取消),可以回调,根据传来的prompt字符串反解出数据,判断是否是所需要的拦截而非常规H5弹框
prompt() 弹出个输入框,让用户输入东西,可以回调
2.利用JS的上下文注入,可以用scriptMessageHandler注入,也可以用WKUserScript WKWebView的addUserScript方法,在加载时机注入
这个实现主要是依靠WKScriptMessageHandler协议类和WKUserContentController两个类:WKUserContentController对象负责注册JS方法,设置处理接收JS方法的代理,代理遵守WKScriptMessageHandler,实现捕捉到JS消息的回调方法
注意:遵守WKScriptMessageHandler协议,代理是由WKUserContentControl设置
//配置对象注入
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeObject"];
//移除对象注入
//[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeObject"];
//苹果WKWebView scriptMessageHandler注入 - 客户端接收调用
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//message.name
//1 解读JS传过来的JSValue data数据
NSDictionary *msgBody = message.body;
//2 取出指令参数,确认要发起的native调用的指令是什么
//3 取出数据参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
}
//在 网页的 js方法中
//window.webkit.messageHandlers.nativeObject.postMessage("")
这里需要注意一下,网页执行的那一行 js 代码,最后一定要返回一个参数,哪怕是一个空字符串也行,否则什么都不传的话,原生方法是不会被执行的
3.还可以用 decidePolicyForNavigationAction 对url 进行拦截判断
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
NSString * urlStr = navigationAction.request.URL.absoluteString;
NSLog(@"发送跳转请求:%@",urlStr);
//自己定义的协议头
NSString *htmlHeadString = @"github://";
if (![urlStr hasPrefix:htmlHeadString]){
//2 取出路径,确认要发起的native调用的指令是什么
//3 取出参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
//确认拦截,拒绝WebView继续发起请求
decisionHandler(WKNavigationActionPolicyCancel);
}else{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"通过截取URL调用OC" message:@"你想前往我的Github主页?" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}])];
[alertController addAction:([UIAlertAction actionWithTitle:@"打开" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSURL * url = [NSURL URLWithString:[urlStr stringByReplacingOccurrencesOfString:@"github://callName_?" withString:@""]];
[[UIApplication sharedApplication] openURL:url];
}])];
[self presentViewController:alertController animated:YES completion:nil];
decisionHandler(WKNavigationActionPolicyAllow);
}
return YES;
}
原生调用JS
有两种方式
1.evaluatingJavaScript
2.WKUserScript
这两种方式的区别
evaluatingJavaScript 是在客户端执行这条代码的时候立刻去执行当条JS代码
WKUserScript 是预先准备好JS代码,当WKWebView加载Dom的时候,执行当条JS代码
第一种方式
//需要在客户端用OC拼接字符串,拼出一个js代码,传递的数据用json
NSString *paramsString = @"{data:xxx,data2:xxx}";
//拼接好的 js代码 calljs('{data:xxx,data2:xxx}');
//其实我们拼接出来的js只是一行js代码,当然无论多长多复杂的js代码都可以用这个方式让webview执行
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//OC调用js的方法,带有回调 要求必须在主线程执行JS
if ([[NSThread currentThread] isMainThread]) {
[self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {
__strong typeof(self)strongSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
[strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
});
}
//在网页端接收参数
//function calljs(data){
// console.log(JSON.parse(data))
// //1 识别客户端传来的数据
// //2 对数据进行分析,从而调用或执行其他逻辑
//}
第二种方式
//在loadurl之前使用 time是一个时机参数,可选dom开始加载/dom加载完毕,2个时机进行执行JS
//构建userscript
WKUserScript *script = [[WKUserScript alloc]initWithSource:source injectionTime:time forMainFrameOnly:mainOnly];
WKUserContentController *userController = webView.userContentController;
//配置userscript
[userController addUserScript:script]