iOS WKWebView與JS交互
WKWebView
iOS8.0之后我們使用 WebKit
框架中的WKWebView
來加載網頁。
WKWebViewConfiguration
來配置JS交互。
其中的和JS交互的功能
-
WKPreferences
(是WKWebViewConfiguration
的屬性) 中的javaScriptEnabled
是Bool實行來打開或者關閉javaScript
*javaScriptCanOpenWindowsAutomatically
Bool控制javaScript
打開windows
。
`WKWebView`中的`navigationDelegate`協議可以監聽加載網頁的周期和結果。
* 判斷鏈接是否允許跳轉
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
```
* 拿到響應后決定是否允許跳轉
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
```
* 鏈接開始加載時調用
```
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
```
* 收到服務器重定向時調用
```
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
```
* 加載錯誤時調用
```
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
```
* 當內容開始到達主幀時被調用(即將完成)
```
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
```
* 加載完成
```
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
```
* 在提交的主幀中發生錯誤時調用
```
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
```
* 當webView需要響應身份驗證時調用(如需驗證服務器證書)
```
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
```
* 當webView的web內容進程被終止時調用。(iOS 9.0之后)
```
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));
```
-
WKWebView
中的WKUIDelegate
實現UI彈出框的一些處理(警告面板、確認面板、輸入框)。
* 在JS端調用alert函數時,會觸發此代理方法。JS端調用alert時所傳的數據可以通過message拿到。在原生得到結果后,需要回調JS,是通過completionHandler回調。
```
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
NSLog(@"message = %@",message);
}
```
* JS端調用confirm函數時,會觸發此方法,通過message可以拿到JS端所傳的數據,在iOS端顯示原生alert得到YES/NO后,通過completionHandler回調給JS端
```
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
{
NSLog(@"message = %@",message);
}
```
* JS端調用prompt函數時,會觸發此方法,要求輸入一段文本,在原生輸入得到文本內容后,通過completionHandler回調給JS
```
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler
{
NSLog(@"%s", __FUNCTION__);
NSLog(@"%@", prompt);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"textinput" message:@"JS調用輸入框" preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField)
{
textField.textColor = [UIColor redColor];
}];
[alert addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action)
{
completionHandler([[alert.textFields lastObject] text]);
}]];
[self presentViewController:alert animated:YES completion:NULL];
}
```
JS交互實現流程
使用WKWebView,JS調iOS-JS端必須使用window.webkit.messageHandlers.JS_Function_Name.postMessage(null)
,其中JS_Function_Name
是iOS端提供個JS交互的Name。
例:
function iOSCallJsAlert()
{
alert('彈個窗,再調用iOS端的JS_Function_Name');
window.webkit.messageHandlers.JS_Function_Name.postMessage({body: 'paramters'});
}
在注入JS交互Handler之后會用到[userContentController addScriptMessageHandler:self name:JS_Function_Name]
。釋放使用到[userContentController removeScriptMessageHandlerForName:JS_Function_Name]
我們JS呼叫iOS通過上面的Handler在iOS本地會有方法獲取到。獲取到之后我們可以根據iOS和JS之間定義好的協議,來做出相應的操作。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
NSLog(@"JS調iOS name : %@ body : %@",message.name,message.body);
}
處理簡單的操作,可以讓JS打開新的web頁面,在WKWebView
的WKNavigationDelegate
協議中,判斷要打開的新的web頁面是否是含有你需要的東西,如果有需要就截獲,不打開并且進行本地操作。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString * url = navigationAction.request.URL.absoluteString;
if ([url hasPrefix:@"alipays://"] || [url hasPrefix:@"alipay://"])
{
if ([[UIApplication sharedApplication] canOpenURL:navigationAction.request.URL])
{
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
if(decisionHandler)
{
decisionHandler(WKNavigationActionPolicyCancel);
}
}
}
}
iOS端調用JS中的函數只需要知道在JS中的函數名稱和函數需要傳遞的參數。通過原生的方法呼叫JS,
iOSCallJsAlert()
是JS端的函數名稱,如果有參數iOS端寫法iOSCallJsAlert('p1','p2')
[webView evaluateJavaScript:@"iOSCallJsAlert()" completionHandler:nil]
JS和iOS注意的地方
①. 上面提到[userContentController addScriptMessageHandler:self name:JS_Function_Name]
是注冊JS的MessageHandler,但是WKWebView在多次調用loadRequest,會出現JS無法調用iOS端。我們需要在loadRequest和reloadWebView的時候需要重新注入。(在注入之前需要移除再注入,避免造成內存泄漏)
如果message.body
中沒有參數,JS代碼中需要傳null
防止iOS端不會接收到JS的交互。
window.webkit.messageHandlers.kJS_Login.postMessage(null)
②. 在WKWebView中點擊沒有反應的時候,可以參考一下處理
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame)
{
[webView loadRequest:navigationAction.request];
}
return nil;
}
③. HTML中不能通過<a href="tel:123456789">撥號</a>
來撥打iOS的電話。需要在iOS端的WKNavigationDelegate
中截取電話在使用原生進行調用撥打電話。其中的[navigationAction.request.URL.scheme isEqualToString:@"tel"]
中的@"tel"
是JS中的定義好,并iOS端需要知道的。發送請求前決定是否跳轉,并在此攔截撥打電話的URL
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
/// <a href="tel:123456789">撥號</a>
if ([navigationAction.request.URL.scheme isEqualToString:@"tel"])
{
decisionHandler(WKNavigationActionPolicyCancel);
NSString * mutStr = [NSString stringWithFormat:@"telprompt://%@",navigationAction.request.URL.resourceSpecifier];
if ([[UIApplication sharedApplication] canOpenURL:mutStr.URL])
{
if (iOS10())
{
[[UIApplication sharedApplication] openURL:mutStr.URL options:@{} completionHandler:^(BOOL success) {}];
}
else
{
[[UIApplication sharedApplication] openURL:mutStr.URL];
}
}
}
else
{
decisionHandler(WKNavigationActionPolicyAllow);
}
}
④. 在執行goBack
或reload
或goToBackForwardListItem
之后請不要馬上執行loadRequest
,使用延遲加載。
⑤在使用中JS端:H5、DOM綁定事件。每一次JS方法調用iOS方法的時候,我都為這個JS方法綁定一個對應的callBack
方法,這樣的話,同時在發送的消息中告訴iOS需要回調,iOS方法就可以執行完相關的方法后,直接回調相應的callBack
方法,并攜帶相關的參數,這樣就可以完美的進行交互了。這是為了在JS調用iOS的時候,在- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
獲取到信息后,iOS端調用[_webView evaluateJavaScript:jsString completionHandler:^(id _Nullable data, NSError * _Nullable error) {}];
給JS發送消息,保證JS在獲取相關返回值時,一定能拿到值。
⑥根據需求清楚緩存和Cookie。
JS端可以參考:漫談js自定義事件、DOM/偽DOM自定義事件
WKWebview加載遠程JS文件和本地JS文件
在頁面請求成功 頁面加載完成之后調用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
completionHandler
中JS
是可以再收到調用之后給webView
回調。
WKWebView遠程網頁加載遠程JS文件
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
[self.webView evaluateJavaScript:@"var script = document.createElement('script');"
"script.type = 'text/javascript';"
"script.src = 'http://www.ohmephoto.com/test.js';"
"document.getElementsByTagName('head')[0].appendChild(script);"
completionHandler:^(id _Nullable object, NSError * _Nullable error)
{
NSLog(@"------error = %@ object = %@",error,object);
}];
}
WKWebView遠程網頁加載本地JS
在xcode
中新建
找到Other
->Empty
,確定文件名XXX.js
一般需要在本地加載的JS
都會很小,用原生JS
直接加載就可以了
題外:看到網友是自定義NSURLProtocol類 - 高端大氣上檔次,請自行查閱。
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
NSString * plistPath = [[NSBundle mainBundle] pathForResource:@"XXX" ofType:@"js"];
NSString * data = [NSString stringWithContentsOfFile:plistPath encoding:NSUTF8StringEncoding error:nil];// [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
[self.webView evaluateJavaScript:[NSString stringWithFormat:@"javascript:%@",data]
completionHandler:^(id _Nullable object, NSError * _Nullable error)
{
}];
}
第三方庫WebViewJavascriptBridge
GitHub地址WebViewJavascriptBridge
不做過多解釋,很好用的第三方庫。安卓也有相應的庫。同樣很強大。
WKWebView進度條
聲明屬性
@property (nonatomic, strong) UIProgressView *progressView;
//進度條初始化
- (UIProgressView *)progressView
{
if (!_progressView)
{
_progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 2)];
_progressView.backgroundColor = [UIColor blueColor];
_progressView.transform = CGAffineTransformMakeScale(1.0f, 1.5f);
_progressView.progressTintColor = [UIColor app_color_yellow_eab201];
[self.view addSubview:self.progressView];
}
return _progressView;
}
給ViewController
中添加Observer
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
在dealloc
找那個刪除Observer
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
- 在
observeValueForKeyPath
中添加對progressView
的進度顯示操作
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"estimatedProgress"])
{
self.progressView.progress = self.webView.estimatedProgress;
if (self.progressView.progress == 1)
{
WeakSelfDeclare
[UIView animateWithDuration:0.25f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^
{
weakSelf.progressView.transform = CGAffineTransformMakeScale(1.0f, 1.4f);
}
completion:^(BOOL finished)
{
weakSelf.progressView.hidden = YES;
}];
}
}
}
- 顯示
progressView
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
{
self.progressView.hidden = NO;
self.progressView.transform = CGAffineTransformMakeScale(1.0f, 1.5f);
[self.view bringSubviewToFront:self.progressView];
}
- 隱藏
progressView
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
self.progressView.hidden = YES;
}
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
if(error.code==NSURLErrorCancelled)
{
[self webView:webView didFinishNavigation:navigation];
}
else
{
self.progressView.hidden = YES;
}
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
self.progressView.hidden = YES;
[self.navigationItem setTitleWithCustomLabel:@"加載失敗"];
}
WKWebView
清楚緩存
有人是這么寫的
- (void)clearCache
{
/* 取得Library文件夾的位置*/
NSString *libraryDir = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask, YES)[0];
/* 取得bundle id,用作文件拼接用*/ NSString *bundleId = [[[NSBundle mainBundle] infoDictionary]objectForKey:@"CFBundleIdentifier"];
/* * 拼接緩存地址,具體目錄為App/Library/Caches/你的APPBundleID/fsCachedData */
NSString *webKitFolderInCachesfs = [NSString stringWithFormat:@"%@/Caches/%@/fsCachedData",libraryDir,bundleId];
NSError *error;
/* 取得目錄下所有的文件,取得文件數組*/
NSFileManager *fileManager = [NSFileManager defaultManager];
//NSArray *fileList = [[NSArray alloc] init];
//fileList便是包含有該文件夾下所有文件的文件名及文件夾名的數組
NSArray *fileList = [fileManager contentsOfDirectoryAtPath:webKitFolderInCachesfs error:&error];
/* 遍歷文件組成的數組*/
for(NSString * fileName in fileList)
{
/* 定位每個文件的位置*/
NSString * path = [[NSBundle bundleWithPath:webKitFolderInCachesfs] pathForResource:fileName ofType:@""];
/* 將文件轉換為NSData類型的數據*/
NSData * fileData = [NSData dataWithContentsOfFile:path];
/* 如果FileData的長度大于2,說明FileData不為空*/
if(fileData.length >2)
{
/* 創建兩個用于顯示文件類型的變量*/
int char1 =0;
int char2 =0;
[fileData getBytes:&char1 range:NSMakeRange(0,1)];
[fileData getBytes:&char2 range:NSMakeRange(1,1)];
/* 拼接兩個變量*/ NSString *numStr = [NSString stringWithFormat:@"%i%i",char1,char2];
/* 如果該文件前四個字符是6033,說明是Html文件,刪除掉本地的緩存*/
if([numStr isEqualToString:@"6033"])
{
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@/%@",webKitFolderInCachesfs,fileName]error:&error]; continue;
}
}
}
}
也可以這樣寫
- (void)cleanCacheAndCookie
{
//清除cookies
NSHTTPCookie *cookie;
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (cookie in [storage cookies])
{
[storage deleteCookie:cookie];
}
[[NSURLCache sharedURLCache] removeAllCachedResponses];
NSURLCache * cache = [NSURLCache sharedURLCache];
[cache removeAllCachedResponses];
[cache setDiskCapacity:0];
[cache setMemoryCapacity:0];
WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
[dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records)
{
for (WKWebsiteDataRecord *record in records)
{
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes
forDataRecords:@[record]
completionHandler:^
{
NSLog(@"Cookies for %@ deleted successfully",record.displayName);
}];
}
}];
}
- (void)dealloc
{
[_webView stopLoading];
[_webView setNavigationDelegate:nil];
[self clearCache];
[self cleanCacheAndCookie];
}
WKWebView
修改userAgent
在項目中我們游戲直接使用以下方式寫入userAgent
,出現了URL可以加載,但是URL里面的資源無法加載問題。但是在微信和外部Safari是可以的。后來查出,不要去直接整個修改掉userAgent
。要在原有的userAgent
加上你需要的userAgent
字符串,進行重新注冊就可以了。(具體原因可能是外部游戲引擎,會默認取系統的userAgent
來做他們的處理,你改掉整個會出現問題)。
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":@"CustomUserAgent"}];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.webView setCustomUserAgent:newUserAgent];
使用下面的修改userAgent
使用NSUserDefaults
修改本地的userAgent
使用WKWebView
的setCustomUserAgent
修改網絡userAgent
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error)
{
NSString * userAgent = result;
NSString * newUserAgent = [userAgent stringByAppendingString:@"CustomUserAgent"];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":newUserAgent}];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.webView setCustomUserAgent:newUserAgent];
}];
WKWebView
重定向問題
在使用過程中,我們獲取到一個鏈接需要webView打開,但是這個鏈接是可以直接重定向到別的地方的。
比如要直接打開AppStore,到相應的App下載頁面,不是打開webView
當我們需要打開的之前,我們用NSURLConnection
來判斷是否有重定向。
代碼如下:
- (void)requestByURLConnectionString:(NSString *)string
{
NSURL *url = [NSURL URLWithString:string];
NSMutableURLRequest *quest = [NSMutableURLRequest requestWithURL:url];
quest.HTTPMethod = @"GET";
NSURLConnection *connect = [NSURLConnection connectionWithRequest:quest delegate:self];
[connect start];
}
#pragma mark - NSURLConnectionDataDelegate
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response
{
NSHTTPURLResponse *urlResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode: %ld", urlResponse.statusCode);
NSDictionary *headers = urlResponse.allHeaderFields;
NSLog(@"%@", headers);
NSLog(@"redirect url: %@", headers[@"Location"]); // 重定向的地址
NSLog(@"newRequest url: %@", [request URL]); // 重定向的地址或原地址
NSLog(@"redirect response url: %@", [urlResponse URL]);// 觸發重定向請求的地址,
if ([request URL] != nil && headers[@"Location"] != nil)
{
有重定向進行處理
}
else
{
無重定向處理
}
return request;
}
WKWebView
時間顯示Nan
問題 (js時間處理)
1 正常的處理如下:
1. var regTime = result.RegTime;
2. var dRegTime = new Date(regTime);
3. var regHtml = dRegTime.getFullYear() + "年" + dRegTime.getMonth() + "月";
在iOS系統下,JS需要正則把-
替換成/
var regTime = result.RegTime.replace(/\-/g, "/");
總結
iOS
中的WKWebView
使用簡單方便。使用它你只用將你用到的進行封裝。在你的ViewController中進行初始化WKWebView
并加載和對其配置,就能完整的使用了。
iOS
端和JS
互相調用,有簡單的函數方法進行互相配合。在交互的時候需要雙方約定好特定的事件名稱。比如登錄、打開支付、彈出分享等常規操作。
JS
向iOS
端發送消息使用window.webkit.messageHandlers.JS_Function_Name.postMessage(null)
。
在iOS
端接受JS
發來的消息需要WKUserContentController
添加Handler
并且處理協議,在協議中判斷并處理JS
端需要iOS
端做的事件。
iOS
調用JS
直接使用WKWebView
的[webView evaluateJavaScript:@"JS函數名稱('參數1','參數2')" completionHandler:nil]
來向JS
發送消息。