WKWebView自出現(xiàn)以來一直被人們所推崇,原因是他的優(yōu)點很多:
更多的支持HTML5的特性,與JS交互更容易
官方宣稱的高達60fps的滾動刷新率以及內(nèi)置手勢
將UIWebViewDelegate與UIWebView拆分成了14類與3個協(xié)議,以前很多不方便實現(xiàn)的功能得以實現(xiàn)。
Safari相同的JavaScript引擎
占用更少的內(nèi)存,加載速度快
但是我一直以來對WKWebView都不是太熟悉,所以最近抽時間學習了一下,并做了總結(jié)
WKWebView有兩個delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要處理一些跳轉(zhuǎn)、加載處理操作,WKUIDelegate主要處理JS腳本,確認框,警告框等。因此WKNavigationDelegate更加常用。
WKWebview提供了API實現(xiàn)js交互 不需要借助JavaScriptCore或者webJavaScriptBridge(由于WKWebView是在一個單獨的進程中運行,我們無法獲取到 JSContext,所以我們無法使用 JSCore 這個強大的框架來進行交互)。使用WKWebViewConfiguration類中的一個屬性WKUserContentController,即userContentController,來實現(xiàn)js native交互。簡單的說就是先注冊約定好的方法,然后再調(diào)用。在 WKWebVeiw 中,我們使用我們有兩種方式來調(diào)用 JS,一種是使用 WKUserScript;另一種是直接調(diào)用 JS 字符串
然后列舉下里面所包含的類和協(xié)議
類
WKBackForwardList 之前訪問過的 web 頁面的列表,可以通過后退和前進動作來訪問到。
WKBackForwardListItem: webview 中后退列表里的某一個網(wǎng)頁。
WKFrameInfo: 包含一個網(wǎng)頁的布局信息。
WKNavigation: 包含一個網(wǎng)頁的加載進度信息。
WKNavigationAction: 包含可能讓網(wǎng)頁導航變化的信息,用于判斷是否做出導航變化。
WKNavigationResponse: 包含可能讓網(wǎng)頁導航變化的返回內(nèi)容信息,用于判斷是否做出導航變化。
WKPreferences: 概括一個 webview 的偏好設置。
WKProcessPool: 表示一個 web 內(nèi)容加載池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網(wǎng)頁發(fā)出的信息。
WKUserScript: 表示可以被網(wǎng)頁接受的用戶腳本。WKWebViewConfiguration: 初始化 webview 的設置。
WKWindowFeatures: 指定加載新網(wǎng)頁時的窗口屬性。
協(xié)議
WKNavigationDelegate: 提供了追蹤主窗口網(wǎng)頁加載過程和判斷主窗口和子窗口是否進行頁面加載新頁面的相關(guān)方法。
WKScriptMessageHandler: 提供從網(wǎng)頁中收消息的回調(diào)方法。
WKUIDelegate: 提供用原生控件顯示網(wǎng)頁的方法回調(diào)。
協(xié)議方法
#pragma mark - WKNavigationDelegate WKNavigationDelegate來追蹤加載過程
// 頁面開始加載時調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
}
// 開始渲染頁面時調(diào)用,響應的內(nèi)容到達主頁面的時候響應,剛準備開始渲染頁面
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
}
// 頁面加載完成之后調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
}
// 頁面加載失敗時調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
}
#pragma mark - WKNavigationDelegate WKNavigtionDelegate來進行頁面跳轉(zhuǎn)
// 接收到服務器跳轉(zhuǎn)請求之后調(diào)用,接收到服務器跳轉(zhuǎn)請求即服務重定向時之后調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
}
// 在收到響應后,決定是否跳轉(zhuǎn)。根據(jù)客戶端受到的服務器響應頭以及response相關(guān)信息來決定是否可以跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationResponsePolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationResponsePolicyCancel);
}
// 在發(fā)送請求之前,決定是否跳轉(zhuǎn),在這個方法里可以對頁面跳轉(zhuǎn)進行攔截處理
- (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) {
//回調(diào)的URL中如果含有百度,就直接返回,也就是關(guān)閉了webView界面
[self.navigationController popViewControllerAnimated:YES];
}
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationActionPolicyCancel);
}
//需要響應身份驗證時調(diào)用 同樣在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 的發(fā)送方提供 credential
[challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
//進程被終止時調(diào)用(當 WKWebView 總體內(nèi)存占用過大,頁面即將白屏時會調(diào)用該方法)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
}
#pragma mark - WKUIDelegate
// 創(chuàng)建一個新的WebView,解決點擊內(nèi)部鏈接沒有反應問題
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
return [[WKWebView alloc]init];
}
//2.WebVeiw關(guān)閉(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是在一個單獨的進程中運行,所以有時候登錄狀態(tài)會丟失,所以需要cookie.
1.在創(chuàng)建的時候存放到WKUserScript中進行添加cookie
//創(chuàng)建配置
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內(nèi)容處理池
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 就是我們要調(diào)用的 JS 函數(shù)
// injectionTime 這個參數(shù)我們需要指定一個時間,在什么時候把我們在這段 JS 注入到 WebVeiw 中,它是一個枚舉值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
// MainFrameOnly 因為在 JS 中,一個頁面可能有多個 frame,這個參數(shù)指定我們的 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];
//打開網(wǎng)頁間的 滑動返回
_webView.allowsBackForwardNavigationGestures =YES;
//滑動減速的速度
_webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
//禁止?jié)L動
//_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)];
//設置進度條的高度,下面這句代碼表示進度條的寬度變?yōu)樵瓉淼?倍,高度變?yōu)樵瓉淼?.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];
//添加監(jiān)測網(wǎng)頁標題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;
}
}
// 記得取消監(jiān)聽
- (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會出現(xiàn)這種情況
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 調(diào)用原生的方式
1.利用 WKUIDelegate 的三個代理方法對 JS 進行攔截
alert() 彈出個提示框,只能點確認無回調(diào)
confirm() 彈出個確認框(確認,取消),可以回調(diào),根據(jù)傳來的prompt字符串反解出數(shù)據(jù),判斷是否是所需要的攔截而非常規(guī)H5彈框
prompt() 彈出個輸入框,讓用戶輸入東西,可以回調(diào)
2.利用JS的上下文注入,可以用scriptMessageHandler注入,也可以用WKUserScript WKWebView的addUserScript方法,在加載時機注入
這個實現(xiàn)主要是依靠WKScriptMessageHandler協(xié)議類和WKUserContentController兩個類:WKUserContentController對象負責注冊JS方法,設置處理接收JS方法的代理,代理遵守WKScriptMessageHandler,實現(xiàn)捕捉到JS消息的回調(diào)方法
注意:遵守WKScriptMessageHandler協(xié)議,代理是由WKUserContentControl設置
//配置對象注入
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeObject"];
//移除對象注入
//[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeObject"];
//蘋果WKWebView scriptMessageHandler注入 - 客戶端接收調(diào)用
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//message.name
//1 解讀JS傳過來的JSValue data數(shù)據(jù)
NSDictionary *msgBody = message.body;
//2 取出指令參數(shù),確認要發(fā)起的native調(diào)用的指令是什么
//3 取出數(shù)據(jù)參數(shù),拿到JS傳過來的數(shù)據(jù)
//4 根據(jù)指令調(diào)用對應的native方法,傳遞數(shù)據(jù)
}
//在 網(wǎng)頁的 js方法中
//window.webkit.messageHandlers.nativeObject.postMessage("")
這里需要注意一下,網(wǎng)頁執(zhí)行的那一行 js 代碼,最后一定要返回一個參數(shù),哪怕是一個空字符串也行,否則什么都不傳的話,原生方法是不會被執(zhí)行的
3.還可以用 decidePolicyForNavigationAction 對url 進行攔截判斷
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//1 根據(jù)url,判斷是否是所需要的攔截的調(diào)用 判斷協(xié)議/域名
NSString * urlStr = navigationAction.request.URL.absoluteString;
NSLog(@"發(fā)送跳轉(zhuǎn)請求:%@",urlStr);
//自己定義的協(xié)議頭
NSString *htmlHeadString = @"github://";
if (![urlStr hasPrefix:htmlHeadString]){
//2 取出路徑,確認要發(fā)起的native調(diào)用的指令是什么
//3 取出參數(shù),拿到JS傳過來的數(shù)據(jù)
//4 根據(jù)指令調(diào)用對應的native方法,傳遞數(shù)據(jù)
//確認攔截,拒絕WebView繼續(xù)發(fā)起請求
decisionHandler(WKNavigationActionPolicyCancel);
}else{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"通過截取URL調(diào)用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;
}
原生調(diào)用JS
有兩種方式
1.evaluatingJavaScript
2.WKUserScript
這兩種方式的區(qū)別
evaluatingJavaScript 是在客戶端執(zhí)行這條代碼的時候立刻去執(zhí)行當條JS代碼
WKUserScript 是預先準備好JS代碼,當WKWebView加載Dom的時候,執(zhí)行當條JS代碼
第一種方式
//需要在客戶端用OC拼接字符串,拼出一個js代碼,傳遞的數(shù)據(jù)用json
NSString *paramsString = @"{data:xxx,data2:xxx}";
//拼接好的 js代碼 calljs('{data:xxx,data2:xxx}');
//其實我們拼接出來的js只是一行js代碼,當然無論多長多復雜的js代碼都可以用這個方式讓webview執(zhí)行
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//OC調(diào)用js的方法,帶有回調(diào) 要求必須在主線程執(zhí)行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];
});
}
//在網(wǎng)頁端接收參數(shù)
//function calljs(data){
// console.log(JSON.parse(data))
// //1 識別客戶端傳來的數(shù)據(jù)
// //2 對數(shù)據(jù)進行分析,從而調(diào)用或執(zhí)行其他邏輯
//}
第二種方式
//在loadurl之前使用 time是一個時機參數(shù),可選dom開始加載/dom加載完畢,2個時機進行執(zhí)行JS
//構(gòu)建userscript
WKUserScript *script = [[WKUserScript alloc]initWithSource:source injectionTime:time forMainFrameOnly:mainOnly];
WKUserContentController *userController = webView.userContentController;
//配置userscript
[userController addUserScript:script]