WKWebView適配中最麻煩的就是cookie同步問題javascript
WKWebView採用了獨立存儲控件,所以和以往的UIWebView並不互通html
雖然iOS11之後,iOS開放了WKHTTPCookieStore讓開發者去同步,可是仍是須要考慮低版本的 同步問題,本章節從各個角度切入考慮cookie同步問題前端
iOS11+java
能夠直接使用WKHTTPCookieStore遍歷方式設值,能夠在建立wkwebview時候就同步也能夠是請求時候ios
// iOS11同步 HTTPCookieStorag到WKHTTPCookieStore WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore; - (void)syncCookiesToWKCookieStore:(WKHTTPCookieStore *)cookieStore API_AVAILABLE(ios(11.0)){ NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; if (cookies.count == 0) return; for (NSHTTPCookie *cookie in cookies) { [cookieStore setCookie:cookie completionHandler:^{ if ([cookies.lastObject isEqual:cookie]) { [self wkwebviewSetCookieSuccess]; } }]; } }
同步cookie能夠在初始化wkwebview的時候,也能夠在請求的時候。初始化時候同步能夠確保發起html頁面請求的時候帶上cookieweb
例如:請求在線頁面時候要經過cookie來認證身份,若是不是初始化時同步,可能請求頁面時就是401了objective-c
iOS11-macos
經過前端執行js注入cookie,在請求時候執行json
//wkwebview執行JS - (void)injectCookiesLT11 { WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; [self.wkWebView.configuration.userContentController addUserScript:cookieScript]; } //遍歷NSHTTPCookieStorage,拼裝JS並執行 - (NSString *)cookieString { NSMutableString *script = [NSMutableString string]; [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"]; for (NSHTTPCookie *cookie in NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies) { // Skip cookies that will break our script if ([cookie.value rangeOfString:@"'"].location != NSNotFound) { continue; } [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, [self formatCookie:cookie]]; } return script; } //Format cookie的js方法 - (NSString *)formatCookie:(NSHTTPCookie *)cookie { NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@", cookie.name, cookie.value, cookie.domain, cookie.path ?: @"/"]; if (cookie.secure) { string = [string stringByAppendingString:@";secure=true"]; } return string; }
可是上面方法執行js,也沒法保證第一個頁面請求帶有cookie數組
因此請求時候建立request須要設置cookie,而且loadRequest
-(void)injectRequestCookieLT11:(NSMutableURLRequest*)mutableRequest { // iOS11如下,手動同步全部cookie NSArray *cookies = NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies; NSMutableArray *mutableCookies = @[].mutableCopy; for (NSHTTPCookie *cookie in cookies) { [mutableCookies addObject:cookie]; } // Cookies數組轉換爲requestHeaderFields NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:(NSArray *)mutableCookies]; // 設置請求頭 mutableRequest.allHTTPHeaderFields = requestHeaderFields; }
wkwebview產生的cookie也可能在某些場景須要同步給NSHTTPCookieStorage
iOS11+能夠直接用WKHTTPCookieStore去同步,
iOS11-能夠採用js端獲取,觸發bridge同步給NSHTTPCookieStorage
可是js同步方式沒法同步httpOnly,因此真的遇到了,仍是要結合服務器等方式去作這個同步。
將代碼準備完畢後調用API便可,回調函數能夠接收js執行結果或者錯誤信息,So Easy。
[self.wkWebView evaluateJavaScript:jsCode completionHandler:^(id object, NSError *error){}];
其實就是提早注入一些JS方法,能夠提供給JS端調用。
好比有的框架會將bridge直接經過這種方式注入到WK的執行環境中,而不是從前端引入JS,這種好處就是假設前端的JS是在線加載,JS服務器掛了或者網絡問題,這樣前端頁面就失去了Naitve的Bridge通訊能力了。
-(instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly; //WKUserScriptInjectionTime說明 typedef NS_ENUM(NSInteger, WKUserScriptInjectionTime) { WKUserScriptInjectionTimeAtDocumentStart, /**文檔開始時候就注入**/ WKUserScriptInjectionTimeAtDocumentEnd /**文檔加載完成時注入**/ } API_AVAILABLE(macos(10.10), ios(8.0));
代理類要實現WKScriptMessageHandler
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler> @property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate; - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate; @end
WKScriptMessageHandler就一個方法
@implementation WeakScriptMessageDelegate - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate { self = [super init]; if (self) { _scriptDelegate = scriptDelegate; } return self; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; }
合適時機(通常初始化)設置代理類,而且指定name
NSString* MessageHandlerName = @"bridge"; [config.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:MessageHandlerName];
執行完上面語句後就會在JS端注入了一個對象"window.webkit.messageHandlers.bridge"
//JS端發送消息,參數最好選用String,比較通用 window.webkit.messageHandlers.bridge.postMessage("type");
而後native端能夠經過WKScriptMessage的body屬性中得到傳入的值
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ if ([message.name isEqualToString:HistoryBridageName]) { } else if ([message.name isEqualToString:MessageHandlerName]) { [self jsToNativeImpl:message.body]; } }
這裏咱們爲何要使用WeakScriptMessageDelegate,而且再設置個delegate指向self(controller),爲何不直接指向?
提示:能夠參考NSTimer的循環引用問題
-(void)_defaultConfig{ WKWebViewConfiguration* config = [WKWebViewConfiguration new]; …… …… …… …… WKUserContentController* userController = [[WKUserContentController alloc] init]; config.userContentController = userController; [self injectHistoryBridge:config]; …… …… …… …… } -(void)injectHistoryBridge:(WKWebViewConfiguration*)config{ [config.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:HistoryBridageName]; NSString *_jsSource = [NSString stringWithFormat: @"(function(history) {\n" " function notify(type) {\n" " setTimeout(function() {\n" " window.webkit.messageHandlers.%@.postMessage(type)\n" " }, 0)\n" " }\n" " function shim(f) {\n" " return function pushState() {\n" " notify('other')\n" " return f.apply(history, arguments)\n" " }\n" " }\n" " history.pushState = shim(history.pushState)\n" " history.replaceState = shim(history.replaceState)\n" " window.addEventListener('popstate', function() {\n" " notify('backforward')\n" " })\n" "})(window.history)\n", HistoryBridageName ]; WKUserScript *script = [[WKUserScript alloc] initWithSource:_jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [config.userContentController addUserScript:script]; }
在iOS8 beta5前,JS和Native這樣通訊設置是不行的,因此能夠採用生命週期中作URL的攔截去解析數據來達到效果,這裏不作贅述,能夠自行參考網上相似UIWebview的橋接原理文章
添加UA
實際過程當中最好只是原有UA上作添加操做,所有替換可能致使服務器的拒絕(安全策略)
日誌中紅線部分是整個模擬器的UA,綠色部門是UA中的ApplicationName部分
iOS9上,WKWebview提供了API能夠設置ua中的ApplicationName
config.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", config.applicationNameForUserAgent, @"arleneConfig"];
所有替換UA
iOS9以上直接能夠指定wkwebview的customUserAgent,iOS9如下的話,設置NSUserDefaults
if (@available(iOS 9.0, *)) { self.wkWebView.customUserAgent = @"Hello My UserAgent"; }else{ [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":@"Hello My UserAgent"}]; [[NSUserDefaults standardUserDefaults] synchronize]; }
wkwebview能夠監控頁面加載進度,相似瀏覽器中打開頁面中的進度條的顯示
頁面切換的時候也會自動更新頁面中設置的title,能夠在實際項目中動態切換容器的title,好比根據切換的title設置navigationItem.title
原理直接經過KVO方式監聽值的變化,而後在回調中處理相關邏輯
//kvo 加載進度 [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; //kvo title [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; /** KVO 監聽具體回調**/ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) { ALLOGF(@"Progress--->%@",[NSNumber numberWithDouble:self.webView.estimatedProgress]); }else if([keyPath isEqualToString:@"title"] && object == self.webview){ self.navigationItem.title = self.webView.title; }else{ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } /**銷燬時候記得移除**/ [self.webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress))]; [self.webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(title))];
下面介紹本身實現的bridge通訊框架,前端無需關心所在容器,框架層作適配。
import {WebBridge} from 'XXX' /** * 方法: WebBridge.call(taskName,options,callback) * 參數說明: * taskName String task的名字,用於Native處理分發任務的標識 * options Object 傳遞的其它參數 * callback function 回調函數 *. 回調參數 * json object native返回的內容 **/ WebBridge.call("Alert",{"content":"彈框內容","btn":"btn內容"},function(json){ console.log("call back is here",JSON.stringify(json)); });
上面調用了Native的Alert控件,而後返回調用結果。
調用到的Native代碼以下:
//AlertTask.m #import "AlertTask.h" #import <lib-base/ALBaseConstants.h> @interface AlertTask (){} @property (nonatomic,weak) ArleneWebViewController* mCtrl; @end @implementation AlertTask -(instancetype)initWithContext:(ArleneWebViewController*)controller{ self = [super init]; self.mCtrl = controller; return self; } -(NSString*)taskName{ return @"Alert"; } -(void)doTask:(NSDictionary*)params{ ALShowAlert(@"Title",@"message");//彈出Alert NSMutableDictionary* callback = [ArleneTaskUtils basicCallback:params];//獲取callback [callback addEntriesFromDictionary:params]; [self.mCtrl callJS:callback];//執行回調 } @end
具體實現原理能夠點擊下方視頻連接: