公司開始讓做一個新iOS項目,由于蘋果的更新需要每次發版本審核,沒法像服務器一樣實時更新,技術部就討論出原生+HTML混合編程的策略。一想到混合編程就毫不猶豫的開始用UIWebView,但是發現點擊按鈕十分卡頓,用戶體驗不是很好。想起蘋果在2014年新出的WKWebView,想試下效果如何,結果讓我很滿意,心里甚是歡喜。但是.......
調用HTML必然牽扯到交互,以前在網上找的很好用的第三方框架(https://github.com/dukeland/EasyJSWebView)不適用WKWebView,剛開始還自以為是的想著把原來的UIWebView的代理方法替換成對應的WKWebView的代理方法就行了,就埋頭在那里替換,替換完之后發現不行,就開始查看WKWebView的官方文檔,還是我的功力太淺,看的一知半解,也不知道應該怎么去做,就只好去網上查找優秀的文章。(我想說的是,我解決問題的思路是有問題的,有點想當然的去做,或許應該先去了解WKWebView,對比著WKWebView和UIWebView的優劣,然后決定用哪個,而不是先做發現問題再去想辦法解決,幸虧現在是把那些坑填滿了。。。如果一開始的選擇是個無底洞,不知道又要做多少無用功了)。以下內容就按照我遇到問題的順序,對WKWebView做一總結:
一? 、WKWebView簡介
WKWebView是現代 WebKit API 在 iOS 8 和 OS X Yosemite 應用中的核心部分。它代替了 UIKit 中的UIWebView和 AppKit 中的WebView,提供了統一的跨雙平臺 API(iOS和OS)。
二、WKWebView新特性
在性能、穩定性、功能方面有很大提升(最直觀的體現就是加載網頁是占用的內存,模擬器加載百度與開源中國網站時,WKWebView占用23M,而UIWebView占用85M);
和 Safari 相同的 JavaScript 引擎,允許JavaScript的Nitro庫加載并使用(UIWebView中限制);
支持了更多的HTML5特性;
自詡擁有 60fps 滾動刷新率、內置手勢、高效的 app 和 web 信息交換通道
將UIWebViewDelegate與UIWebView重構成了14類與3個協議(具體查看蘋果官方文檔);
三、WKWebView不足
WKWebView會忽視默認的網絡存儲, NSURLCache, NSHTTPCookieStorage, NSCredentialStorage。WKWebView有自己的進程,同樣也有自己的存儲空間用來存儲cookie和cache, 其他的網絡類如NSURLConnection是無法訪問到的。同時WKWebView發起的資源請求也是不經過NSURLProtocol的,導致無法自定義請求。同一個應用,不同WKWebView之間的cookie同步,可以通過使用同一個WKProcessPool實現cookie的同步。(我沒有具體實現,如果想嘗試,可以參考http://mcfeng.me/blog/2014/11/03/wkwebview-uiwebview-cookie-sync/)
簡單來說就是頁面之間cookie同步和共享,以前同一個應用,不同UIWebView之間的Cookie是自動同步的。它們都是保存在NSHTTPCookieStorage中。當UIWebView加載一個URL的時候,在加載完成時候,這個URL response中包含的cookie會自動以NSHttpCookie的形式保存到NSHTTPCookieStorage中。同時,如果在http response中,對cookie進行更新或者刪除的話,其結果也會直接反應到NSHTTPCookieStorage存儲的cookie數據中。所以,如果需要取出cookie數值,只要調用[NSHTTPCookieStorage sharedHTTPCookieStorage]即可。但是現在需要手動的設置cookie來保持頁面之間信息同步。
四、WKWebView與HTML交互
WKWebView調用js腳本(以下為示例代碼,作為參考)
```
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
NSString *executeString = @"$(document).ready(function(){$('.guanliansearch').removeClass(\"hide\").addClass(\"show\") });";
[self.webView evaluateJavaScript:executeString completionHandler:^(id _Nullable result, NSError * _Nullable error) {
}];
}
```
HTML調用OC方法
```
UIViewController<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>中WKWebView的初始化,注意要遵守協議
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
[configuration.userContentController addScriptMessageHandler:self name:@"goBack"];(goBack為方法名,與html中對應)
self.webView = [[WKWebView alloc] initWithFrame:viewRect configuration:configuration];
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
self.webView.customUserAgent = kCustomUserAgent;
//讓HTML適配屏幕
self.edgesForExtendedLayout = UIRectEdgeNone;
self.automaticallyAdjustsScrollViewInsets = NO;
[self.view addSubview:self.webView];
//顯示加載頁
[self showLoadingView];
// 1. URL 定位資源,需要資源的地址
NSURL *url = [NSURL URLWithString:self.webViewUrl];
// 2. 把URL告訴給服務器,請求,從m.baidu.com請求數據
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設置緩存策略
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
// 3. 發送請求給服務器
[self.webView loadRequest:request];
}```
```
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSLog(@"%@",NSStringFromSelector(_cmd));
NSLog(@"message.body--%@? message.name--%@",message.body,message.name);
NSArray *messageArray = (NSArray *)message.body;(如果請求為多個參數,強轉為數組)
if ([message.name isEqualToString:@"goBack"]){
[self? goBack];
}
}
- (void)goBack{
[self dismissViewControllerAnimated:YES completion:^{
}];
}```
HTML對應的配置
```
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript">//JS響應方法列表>
function btnClick1() {
var useragent = navigator.userAgent;
if(useragent == “***”){
window.webkit.messageHandlers.goBack.postMessage(null);(沒有參數的話必須寫null,如果不寫按鈕點擊也沒有反應,如果多個參數寫法( ['param1','param2'])? ,即用數組格式;一個參數格式(‘param1’),goBack為方法名)
}
}</script>
<style type="text/css">body {background-color:white;font-size:20px;padding:10;}</style>
li {padding:5}
button { width:700; height:100;margin:10}
</head>
<body><ul><li><button type="button" onclick="btnClick1()">showToast></button></li></ul></body>
</html>
```
五,WKWebView? 的WKUIDelegate方法的實現
```
#pragma mark - WKUIDelegate(我只是實現了2個作為參考,3個方法分別對應JavaScript 中創建三種消息框:警告框、確認框、提示框,如果不實現相應的方法,則HTML中寫的消息框無反應)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
WBLog(@"webView.URL---%@",webView.URL);
UIAlertController *alertController1 = [UIAlertController alertControllerWithTitle:message message: nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *noAction? = [UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();(該方法一定要實現,否則會系統會崩潰,錯誤日志很奇葩)
}];
[alertController1? addAction:noAction];
[self presentViewController:alertController1? animated:YES completion:^{
}];
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message: nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction? = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(WKNavigationResponsePolicyCancel);
}];
[alertController? addAction:cancelAction];
[self presentViewController:alertController? animated:YES completion:^{
}];
UIAlertAction *okAction? = [UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(WKNavigationResponsePolicyAllow);
}];
[alertController? addAction:okAction];
}
```
五,WKWebView? 的cookie共享
說道cookie肯定會想到session,首先簡單了解下:(參考資料:http://blog.csdn.net/fangaoxin/article/details/6952954/)
會話(Session)跟蹤是Web程序中常用的技術,用來跟蹤用戶的整個會話。常用的會話跟蹤技術是Cookie與Session。Cookie通過在客戶端記錄信息確定用戶身份,Session通過在服務器端記錄信息確定用戶身份。
Cookie意為“甜餅”,是由W3C組織提出,最早由Netscape社區發展的一種機制。目前Cookie已經成為標準,所有的主流瀏覽器如IE、Netscape、Firefox、Opera等都支持Cookie。
由于HTTP是一種無狀態的協議,服務器單從網絡連接上無從知道客戶身份。怎么辦呢?就給客戶端們頒發一個通行證吧,每人一個,無論誰訪問都必須攜帶自己通行證。這樣服務器就能從通行證上確認客戶身份了。這就是Cookie的工作原理。
Session是另一種記錄客戶狀態的機制,不同的是Cookie保存在客戶端瀏覽器中,而Session保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在服務器上。這就是Session。客戶端瀏覽器再次訪問時只需要從該Session中查找該客戶的狀態就可以了。
如果說Cookie機制是通過檢查客戶身上的“通行證”來確定客戶身份的話,那么Session機制就是通過檢查服務器上的“客戶明細表”來確認客戶身份。Session相當于程序在服務器上建立的一份客戶檔案,客戶來訪的時候只需要查詢客戶檔案表就可以了。
以下是我手動設置cookie的代碼,(希望有更好的方式,可以多多指教),思路是,當用戶登錄完成之后,保存頁面返回的cookie,主要三個屬性,JSESSIONID,uid,token
用戶登錄完成之后,每次訪問頁面都攜帶有這3個屬性的cookie
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
//每次都存最新的sessionId
[SettingBaseTool deleteDatasByKey:[cookie name]];
[SettingBaseTool saveBaseInfoStringWithKey:[cookie name] value:[cookie value] isDelayUserId:NO];
}
//跳轉之前傳入cookie
//js函數
NSString *JSFuncString =
@"function setCookie(name,value,expires)\
{\
var oDate=new Date();\
oDate.setDate(oDate.getDate()+expires);\
document.cookie=name+'='+value+';expires='+oDate;\
}\
function getCookie(name)\
{\
var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)'));\
if(arr != null) return unescape(arr[2]); return null;\
}\
function delCookie(name)\
{\
var exp = new Date();\
exp.setTime(exp.getTime() - 1);\
var cval=getCookie(name);\
if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString();\
}";
//拼湊js字符串
//取出 JSESSIONID? uid token
NSString *jessionString = @"JSESSIONID";
NSString *uidString = @"uid";
NSString *tokenString = @"token";
NSMutableString *JSCookieString = JSFuncString.mutableCopy;
NSString *jessionValueString = [SettingBaseTool readBaseInfoStringWithKey:jessionString isDelayUserId:NO];(從我自己封裝的方法從沙盒中取出值)
if ([jessionValueString isNotEmpty]) {
NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", jessionString, jessionValueString];
[JSCookieString appendString:excuteJSString];
}
NSString *uidValueString = [SettingBaseTool readBaseInfoStringWithKey:uidString isDelayUserId:NO];
if ([uidValueString isNotEmpty]) {
NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", uidString, uidValueString];
[JSCookieString appendString:excuteJSString];
}
NSString? *tokenVauleString = [SettingBaseTool readBaseInfoStringWithKey:tokenString isDelayUserId:NO];
if ([tokenVauleString isNotEmpty]) {
NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", tokenString, tokenVauleString];
[JSCookieString appendString:excuteJSString];
}
//執行js
[webView evaluateJavaScript:JSCookieString completionHandler:^(id _Nullable result, NSError * _Nullable error) {
decisionHandler(WKNavigationResponsePolicyAllow);
}];
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
NSString *str = @"document.cookie";(獲取頁面返回的cookie,然后設置更新)
[webView evaluateJavaScript:str completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if ([result isNotEmpty]) {
NSArray *resultArray? =[result componentsSeparatedByString:@";"];
if (resultArray.count > 0) {
for (NSString *resultString in resultArray) {
NSArray *keyValueArray = [resultString componentsSeparatedByString:@"="];
if (keyValueArray.count > 1) {
[SettingBaseTool updateDatasByKey:[keyValueArray[0] stringByReplacingOccurrencesOfString:@" " withString:@""] value:keyValueArray[1] ];(自己封裝的方法,更新三個屬性)
}
}
}
}
}];
```
六,捕獲<a href:"9998989">
//在發送請求之前,決定是否跳轉? 如果不實現這個代理方法,默認會屏蔽掉打電話等url
```
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 允許跳轉
decisionHandler(WKNavigationActionPolicyAllow);
}
//頁面開始加載
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
NSString *path=[webView.URL absoluteString];
NSString * newPath = [path lowercaseString];
//捕獲URL,然后用UIApplication打開
if ([newPath hasPrefix:@"sms:"] || [newPath hasPrefix:@"tel:"] || [newPath hasPrefix:@"apipays:"] || [newPath hasPrefix:@"mqqwpa:"]) {
UIApplication * app = [UIApplication sharedApplication];
if ([app canOpenURL:[NSURL URLWithString:newPath]]) {
[app openURL:[NSURL URLWithString:newPath]];
}
return;
}
}
```
七,調用第三方,QQ,或者支付寶
點擊QQ和支付寶都沒有反應,思考原因,最后發現由于自定義了
self.webView.customUserAgent? = @“hah-ios”而第三方判斷是瀏覽器還是Android或者iPhone的標準是基于默認值,所以就改成如下
```
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSString *defaultAgent = (NSString *)result;
self.webView.customUserAgent = [NSString stringWithFormat:@"%@ hah-ios",defaultAgent];
}];
```
當然代碼有些冗余,有待優化。不過有種撥開烏云見明月的感覺。。頓時感覺壓力小了很多。。。