Cookie 问题是目前 WKWebView 的一大短板,WKWebView 是单独进程,拥有自己的私有存储,Cookie 存在 WKHTTPCookieStore 中,每隔一段时间就和app侧NSHTTPCookieStorage进行同步,这个同步是进程级别的同步,WKWebView那些坑中表示“二者同步是单向的”,但是经过验证(iOS 14系统),二者同步是相互的,NSHTTPCookieStorage中会向WKHTTPCookieStore中同步,WKHTTPCookieStore也会向NSHTTPCookieStorage中同步,只不过都存在延时。
其中服务器 set-cookie 或 JS 执行 document.cookie 操作会很快将 Cookie 同步到 NSHTTPCookieStorage 和 WKHTTPCookieStore 中,需要注意的是,对于http-only为true的,document.cookie无法获取到。
WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。
1. WKProcessPool
苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。
2. Cookie同步方案
iOS 11系统官方提供了同步 Cookie 的API,故 iOS 11 系统前后区分处理。
iOS 11及以后系统
iOS 11 及之后的系统可通过 WKHTTPCookieStore 来管理 HTTP Cookie 信息,WKHTTPCookieStore 存储在 WKWebsiteDataStore 中,关系如下:
WKWebViewConfiguration -> WKWebsiteDataStore -> WKHTTPCookieStore
@interface WKWebViewConfiguration : NSObject <NSSecureCoding, NSCopying>
@property (nonatomic, strong) WKWebsiteDataStore *websiteDataStore API_AVAILABLE(macos(10.11), ios(9.0));
@end
@interface WKWebsiteDataStore : NSObject <NSSecureCoding>
+ (WKWebsiteDataStore *)defaultDataStore;
+ (WKWebsiteDataStore *)nonPersistentDataStore;
@property (nonatomic, readonly) WKHTTPCookieStore *httpCookieStore API_AVAILABLE(macos(10.13), ios(11.0));
@end
其中
WKWebsiteDataStore.defaultDataStore
是单例,WKWebViewConfiguration.websiteDataStore
默认值为WKWebsiteDataStore.defaultDataStore
,不管创建多少个WKWebViewConfiguration
,所有的WKWebsiteDataStore
均为全局单例,自己设置的nonPersistentDataStore除外。
综上我们有两个时机去设置 Cookie,一个是初始化 WKWebview 时,一个是发起请求时。注意 WKHTTPCookieStore 设置 Cookie 是一个异步的操作,所以在页面初始化时设置 Cookie 会存在发起请求时,Cookie 没有全部设置完毕的情况。
较好的方案为在 loadRequest 时,先设置 Cookie ,在设置成功的回调中再发起网络请求。
如果没有首页使用 WKWebView 的场景,不需要启动是就加载 web页面,可以在APP启动时,先进行 Cookie 的同步,因为是单例,后续所有的 WKWebView 都会携带对应的 Cookie 。
iOS 11 之前的系统
iOS 11 系统前 Cookie 的设置主要分为两个阶段,第一阶段是 LoadRequest 请求 URL 时需要携带 Cookie ,第二阶段是页面加载完成后,业务请求需要携带 Cookie 。
由于 iOS 11 之前系统没有提供对应API,所以我们需要分别处理这两个阶段。
阶段一:在 loadRequest 前,在 request header 中设置 Cookie ,解决首个请求 Cookie 丢失问题。
WKWebView *webView = [[WKWebView alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://my.cookie.test:8080/WKCookieTestIndex"]];
NSArray *availableCookie = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL];
if (availableCookie.count > 0) {
NSDictionary *reqHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:availableCookie];
NSString *cookieStr = [reqHeader objectForKey:@"Cookie"];
[request setValue:cookieStr forHTTPHeaderField:@"Cookie"];
}
[webView loadRequest:request];
阶段二:通过 document.cookie 设置 Cookie 解决页面内(同域)Ajax、iframe 请求携带 Cookie 问题。
// 可在初始化时进行设置
- (NSString *)ajaxCookieScripts {
NSMutableString *cookieScript = [[NSMutableString alloc] init];
// 为JS增加设置、获取、删除Cookie的方法(需要用到删除方法)
NSString *JSCookieFuncString =
@"function setCookie(name,value,expires)\
{\
var oDate=new Date();\
oDate.setDate(oDate.getDate()+expires);\
document.cookie=name+'='+value+';expires='+oDate+';path=/'\
}\
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();\
}";
[cookieScript appendString:JSCookieFuncString];
// 遍历 HTTPCookieStorage 中所有 Cookie,进行同步
// Tips:系统会根据URL的Domain,自动判断携带Cookie,所以我们设置Cookie时不需要判断域名。
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
// Skip cookies that will break our script
if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
continue;
}
// 设置Cookie前,先进行移除操作,防止出现重复设置的情况
[cookieScript appendFormat:@"delCookie('%@');", cookie.name];
// Create a line that appends this cookie to the web view's document's cookies
[cookieScript appendFormat:@"document.cookie = '%@=%@;", cookie.name, cookie.value];
if (cookie.domain || cookie.domain.length > 0) {
[cookieScript appendFormat:@"domain=%@;", cookie.domain];
}
if (cookie.path || cookie.path.length > 0) {
[cookieScript appendFormat:@"path=%@;", cookie.path];
}
if (cookie.expiresDate) {
[cookieScript appendFormat:@"expires=%@;", cookie.properties[@"Expires"]];
}
if (cookie.secure) {
// 只有 https 请求才能携带该 cookie
[cookieScript appendString:@"Secure;"];
}
if (cookie.HTTPOnly) {
// 保持 native 的 cookie 完整性,当 HTTPOnly 时,不能通过 document.cookie 来读取该 cookie。
[cookieScript appendString:@"HTTPOnly;"];
}
[cookieScript appendFormat:@"';"];
}
return cookieScript;
}
// 设置 WKUserScript
WKUserContentController *userContentController = [WKUserContentController new];
// 注意:此处 injectionTime 要设置成 WKUserScriptInjectionTimeAtDocumentStart,表示在最开始执行。
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:self.ajaxCookieScripts injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
设置Cookie时不删除造成重复
3. 实战经验
项目实战发现,iOS 11前在初始化WKWebView时通过 WKUserScript 设置Cookie ,当 WKUserScript 设置过多时,会影响前端的解析渲染速度,正常业务使用无太大影响。对前端性能要求较高的业务,可以客户端与前端配合,前端页面渲染完成后,在适当的时机,调用原生方法获取Cookie,前端通过 document.cookie 进行手动设置,此方案优势是可以精准控制Cookie,加快首屏渲染速度,弊端是无法做到前端无感知,需要前端配合。可根据实际情况进行选择。
网友评论