iOS WKWebView與JS交互
WKWebView
iOS8.0之后我們使用 WebKit框架中的WKWebView來加載網(wǎng)頁。
WKWebViewConfiguration來配置JS交互。
其中的和JS交互的功能
-
WKPreferences(是WKWebViewConfiguration的屬性) 中的javaScriptEnabled是Bool實(shí)行來打開或者關(guān)閉javaScript
*javaScriptCanOpenWindowsAutomaticallyBool控制javaScript打開windows。
`WKWebView`中的`navigationDelegate`協(xié)議可以監(jiān)聽加載網(wǎng)頁的周期和結(jié)果。
* 判斷鏈接是否允許跳轉(zhuǎn)
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
```
* 拿到響應(yīng)后決定是否允許跳轉(zhuǎn)
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
```
* 鏈接開始加載時(shí)調(diào)用
```
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
```
* 收到服務(wù)器重定向時(shí)調(diào)用
```
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
```
* 加載錯(cuò)誤時(shí)調(diào)用
```
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
```
* 當(dāng)內(nèi)容開始到達(dá)主幀時(shí)被調(diào)用(即將完成)
```
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
```
* 加載完成
```
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
```
* 在提交的主幀中發(fā)生錯(cuò)誤時(shí)調(diào)用
```
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
```
* 當(dāng)webView需要響應(yīng)身份驗(yàn)證時(shí)調(diào)用(如需驗(yàn)證服務(wù)器證書)
```
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
```
* 當(dāng)webView的web內(nèi)容進(jìn)程被終止時(shí)調(diào)用。(iOS 9.0之后)
```
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));
```
-
WKWebView中的WKUIDelegate實(shí)現(xiàn)UI彈出框的一些處理(警告面板、確認(rèn)面板、輸入框)。
* 在JS端調(diào)用alert函數(shù)時(shí),會(huì)觸發(fā)此代理方法。JS端調(diào)用alert時(shí)所傳的數(shù)據(jù)可以通過message拿到。在原生得到結(jié)果后,需要回調(diào)JS,是通過completionHandler回調(diào)。
```
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
NSLog(@"message = %@",message);
}
```
* JS端調(diào)用confirm函數(shù)時(shí),會(huì)觸發(fā)此方法,通過message可以拿到JS端所傳的數(shù)據(jù),在iOS端顯示原生alert得到Y(jié)ES/NO后,通過completionHandler回調(diào)給JS端
```
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
{
NSLog(@"message = %@",message);
}
```
* JS端調(diào)用prompt函數(shù)時(shí),會(huì)觸發(fā)此方法,要求輸入一段文本,在原生輸入得到文本內(nèi)容后,通過completionHandler回調(diào)給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調(diào)用輸入框" 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交互實(shí)現(xiàn)流程
使用WKWebView,JS調(diào)iOS-JS端必須使用window.webkit.messageHandlers.JS_Function_Name.postMessage(null),其中JS_Function_Name是iOS端提供個(gè)JS交互的Name。
例:
function iOSCallJsAlert()
{
alert('彈個(gè)窗,再調(diào)用iOS端的JS_Function_Name');
window.webkit.messageHandlers.JS_Function_Name.postMessage({body: 'paramters'});
}
在注入JS交互Handler之后會(huì)用到[userContentController addScriptMessageHandler:self name:JS_Function_Name]。釋放使用到[userContentController removeScriptMessageHandlerForName:JS_Function_Name]
我們JS呼叫iOS通過上面的Handler在iOS本地會(huì)有方法獲取到。獲取到之后我們可以根據(jù)iOS和JS之間定義好的協(xié)議,來做出相應(yīng)的操作。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
NSLog(@"JS調(diào)iOS name : %@ body : %@",message.name,message.body);
}
處理簡單的操作,可以讓JS打開新的web頁面,在WKWebView的WKNavigationDelegate協(xié)議中,判斷要打開的新的web頁面是否是含有你需要的東西,如果有需要就截獲,不打開并且進(jìn)行本地操作。
- (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端調(diào)用JS中的函數(shù)只需要知道在JS中的函數(shù)名稱和函數(shù)需要傳遞的參數(shù)。通過原生的方法呼叫JS,
iOSCallJsAlert()是JS端的函數(shù)名稱,如果有參數(shù)iOS端寫法iOSCallJsAlert('p1','p2')
[webView evaluateJavaScript:@"iOSCallJsAlert()" completionHandler:nil]
JS和iOS注意的地方
①. 上面提到[userContentController addScriptMessageHandler:self name:JS_Function_Name]是注冊JS的MessageHandler,但是WKWebView在多次調(diào)用loadRequest,會(huì)出現(xiàn)JS無法調(diào)用iOS端。我們需要在loadRequest和reloadWebView的時(shí)候需要重新注入。(在注入之前需要移除再注入,避免造成內(nèi)存泄漏)
如果message.body中沒有參數(shù),JS代碼中需要傳null防止iOS端不會(huì)接收到JS的交互。
window.webkit.messageHandlers.kJS_Login.postMessage(null)
②. 在WKWebView中點(diǎn)擊沒有反應(yīng)的時(shí)候,可以參考一下處理
-(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">撥號(hào)</a>來撥打iOS的電話。需要在iOS端的WKNavigationDelegate中截取電話在使用原生進(jìn)行調(diào)用撥打電話。其中的[navigationAction.request.URL.scheme isEqualToString:@"tel"]中的@"tel"是JS中的定義好,并iOS端需要知道的。發(fā)送請求前決定是否跳轉(zhuǎn),并在此攔截?fù)艽螂娫挼腢RL
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
/// <a href="tel:123456789">撥號(hào)</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);
}
}
④. 在執(zhí)行goBack或reload或goToBackForwardListItem之后請不要馬上執(zhí)行loadRequest,使用延遲加載。
⑤在使用中JS端:H5、DOM綁定事件。每一次JS方法調(diào)用iOS方法的時(shí)候,我都為這個(gè)JS方法綁定一個(gè)對應(yīng)的callBack方法,這樣的話,同時(shí)在發(fā)送的消息中告訴iOS需要回調(diào),iOS方法就可以執(zhí)行完相關(guān)的方法后,直接回調(diào)相應(yīng)的callBack方法,并攜帶相關(guān)的參數(shù),這樣就可以完美的進(jìn)行交互了。這是為了在JS調(diào)用iOS的時(shí)候,在- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message獲取到信息后,iOS端調(diào)用[_webView evaluateJavaScript:jsString completionHandler:^(id _Nullable data, NSError * _Nullable error) {}];給JS發(fā)送消息,保證JS在獲取相關(guān)返回值時(shí),一定能拿到值。
⑥根據(jù)需求清楚緩存和Cookie。
JS端可以參考:漫談js自定義事件、DOM/偽DOM自定義事件
WKWebview加載遠(yuǎn)程JS文件和本地JS文件
在頁面請求成功 頁面加載完成之后調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
completionHandler中JS是可以再收到調(diào)用之后給webView回調(diào)。
WKWebView遠(yuǎn)程網(wǎng)頁加載遠(yuǎn)程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遠(yuǎn)程網(wǎng)頁加載本地JS
在xcode中新建找到Other->Empty,確定文件名XXX.js
一般需要在本地加載的JS都會(huì)很小,用原生JS直接加載就可以了
題外:看到網(wǎng)友是自定義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
不做過多解釋,很好用的第三方庫。安卓也有相應(yīng)的庫。同樣很強(qiáng)大。
WKWebView進(jìn)度條
聲明屬性
@property (nonatomic, strong) UIProgressView *progressView;
//進(jìn)度條初始化
- (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找那個(gè)刪除Observer
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
- 在
observeValueForKeyPath中添加對progressView的進(jìn)度顯示操作
- (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;
/* 取得目錄下所有的文件,取得文件數(shù)組*/
NSFileManager *fileManager = [NSFileManager defaultManager];
//NSArray *fileList = [[NSArray alloc] init];
//fileList便是包含有該文件夾下所有文件的文件名及文件夾名的數(shù)組
NSArray *fileList = [fileManager contentsOfDirectoryAtPath:webKitFolderInCachesfs error:&error];
/* 遍歷文件組成的數(shù)組*/
for(NSString * fileName in fileList)
{
/* 定位每個(gè)文件的位置*/
NSString * path = [[NSBundle bundleWithPath:webKitFolderInCachesfs] pathForResource:fileName ofType:@""];
/* 將文件轉(zhuǎn)換為NSData類型的數(shù)據(jù)*/
NSData * fileData = [NSData dataWithContentsOfFile:path];
/* 如果FileData的長度大于2,說明FileData不為空*/
if(fileData.length >2)
{
/* 創(chuàng)建兩個(gè)用于顯示文件類型的變量*/
int char1 =0;
int char2 =0;
[fileData getBytes:&char1 range:NSMakeRange(0,1)];
[fileData getBytes:&char2 range:NSMakeRange(1,1)];
/* 拼接兩個(gè)變量*/ NSString *numStr = [NSString stringWithFormat:@"%i%i",char1,char2];
/* 如果該文件前四個(gè)字符是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
在項(xiàng)目中我們游戲直接使用以下方式寫入userAgent,出現(xiàn)了URL可以加載,但是URL里面的資源無法加載問題。但是在微信和外部Safari是可以的。后來查出,不要去直接整個(gè)修改掉userAgent。要在原有的userAgent加上你需要的userAgent字符串,進(jìn)行重新注冊就可以了。(具體原因可能是外部游戲引擎,會(huì)默認(rèn)取系統(tǒng)的userAgent來做他們的處理,你改掉整個(gè)會(huì)出現(xiàn)問題)。
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":@"CustomUserAgent"}];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.webView setCustomUserAgent:newUserAgent];
使用下面的修改userAgent
使用NSUserDefaults修改本地的userAgent
使用WKWebView的setCustomUserAgent修改網(wǎng)絡(luò)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重定向問題
在使用過程中,我們獲取到一個(gè)鏈接需要webView打開,但是這個(gè)鏈接是可以直接重定向到別的地方的。
比如要直接打開AppStore,到相應(yīng)的App下載頁面,不是打開webView
當(dāng)我們需要打開的之前,我們用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]);// 觸發(fā)重定向請求的地址,
if ([request URL] != nil && headers[@"Location"] != nil)
{
有重定向進(jìn)行處理
}
else
{
無重定向處理
}
return request;
}
WKWebView時(shí)間顯示Nan問題 (js時(shí)間處理)
1 正常的處理如下:
1. var regTime = result.RegTime;
2. var dRegTime = new Date(regTime);
3. var regHtml = dRegTime.getFullYear() + "年" + dRegTime.getMonth() + "月";
在iOS系統(tǒng)下,JS需要正則把-替換成/
var regTime = result.RegTime.replace(/\-/g, "/");
總結(jié)
iOS中的WKWebView使用簡單方便。使用它你只用將你用到的進(jìn)行封裝。在你的ViewController中進(jìn)行初始化WKWebView并加載和對其配置,就能完整的使用了。
iOS端和JS互相調(diào)用,有簡單的函數(shù)方法進(jìn)行互相配合。在交互的時(shí)候需要雙方約定好特定的事件名稱。比如登錄、打開支付、彈出分享等常規(guī)操作。
JS向iOS端發(fā)送消息使用window.webkit.messageHandlers.JS_Function_Name.postMessage(null)。
在iOS端接受JS發(fā)來的消息需要WKUserContentController添加Handler并且處理協(xié)議,在協(xié)議中判斷并處理JS端需要iOS端做的事件。
iOS調(diào)用JS直接使用WKWebView的[webView evaluateJavaScript:@"JS函數(shù)名稱('參數(shù)1','參數(shù)2')" completionHandler:nil]來向JS發(fā)送消息。