文章目录
  1. 1. WKWebView概述
  2. 2. • WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势1. 更多的支持HTML5的特性2. 高达60fps的滚动刷新率以及内置手势3. 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议4. Safari相同的JS引擎5. 占用更少的内存
  3. 3. 初步解决方案
    1. 3.1. 1、获取Cookie
    2. 3.2. 2、注入Cookie
  4. 4. AFNetWork获取cookie
  5. 5. Cordova集成保持cookie免登陆
  6. 6. wk做离线缓存
  7. 7. 网络辟谣
  8. 8. webview怎么拦截
  9. 9. ajax请求post的body丢失问题

WKWebView概述

• WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势
1. 更多的支持HTML5的特性
2. 高达60fps的滚动刷新率以及内置手势
3. 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议
4. Safari相同的JS引擎
5. 占用更少的内存

首先简单熟悉一下WKWebView的属性方法
// webview 配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;

//配置初始化方法
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

//WKPreferences偏好设置
config.preferences = [[WKPreferences alloc] init];
// 默认为0
config.preferences.minimumFontSize = 10;
// 默认认为YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示不能自动通过窗口打开
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;

// 导航代理 
@property (nullable, nonatomic, weak) id <WKNavigationDelegate>navigationDelegate;

// 用户交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;

// 与UIWebView一样的加载请求API
(nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

// 直接加载HTML
(nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

// 直接加载data
(nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);

// 停止加载数据
(void)stopLoading;

// 执行JS代码
(void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

• JS和WebView内容交互
// 只读属性,所有添加的WKUserScript都在这里可以获取到
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;

// 注入JS
(void)addUserScript:(WKUserScript *)userScript;

// 移除所有注入的JS
(void)removeAllUserScripts;

// 添加scriptMessageHandler到所有的frames中,则都可以通过
// window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
// 发送消息
// JS要调用原生的方法的方式
(void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

// 根据name移除所注入的scriptMessageHandler
(void)removeScriptMessageHandlerForName:(NSString *)name;

• WKUserScript
在WKUserContentController中,所有使用到WKUserScript。WKUserContentController是用于与JS交互的类,而所注入的JS是WKUserScript对象。它的所有属性和方法如下:

// JS源代码
@property (nonatomic, readonly, copy) NSString *source;

// JS注入时间
@property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime;

// 只读属性,表示JS是否应该注入到所有的frames中还是只有main frame.
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOLforMainFrameOnly;

// 初始化方法,用于创建WKUserScript对象
// source:JS源代码
// injectionTime:JS注入的时间
// forMainFrameOnly:是否只注入main frame
(instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;

• WKNavigationDelegate

@protocol WKNavigationDelegate <NSObject>

@optional
// 决定导航的动作,通常用于处理跨域的链接能否导航。WebKit对跨域进行了安全检查限制,不允许跨域,因此我们要对不能跨域的链接
// 单独处理。但是,对于Safari是允许跨域的,不用这么处理。
// 这个是决定是否Request
(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

// 决定是否接收响应
// 这个是决定是否接收response
// 要获取response,通过WKNavigationResponse对象获取
(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

// 当main frame的导航开始请求时,会调用此方法
(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

// 当main frame接收到服务重定向时,会回调此方法
(void)webView:(WKWebView *)webViewdidReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation*)navigation;

// 当main frame开始加载数据失败时,会回调
(void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

// 当main frame的web内容开始到达时,会回调
(void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;

// 当main frame导航完成时,会回调
(void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

// 当main frame最后下载数据失败时,会回调
(void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

// 这与用于授权验证的API,与AFN、UIWebView的授权验证API是一样的
(void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;

// 当web content处理完成时,会回调
(void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);

@end

typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {
WKNavigationActionPolicyCancel,
WKNavigationActionPolicyAllow,
} NS_ENUM_AVAILABLE(10_10, 8_0);

初步解决方案

其实主要要做的只有两步,1、获取Cookie,2、注入Cookie

1、获取Cookie

• 由于 WKWebView 的 Cookie 存储容器 WKWebsiteDataStore 是私有存储,所以无法从这里获取到Cookie,目前的方法是(1)从网站返回的 response headerfields 中获取。(2)通过调用js的方法获取 cookie。

• (1)从网站返回的 response headerfields 中获取

• 因为cookie都存在http respone的headerfields,找到能获得respone的WKWebView回调,打印

(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];
    //读取wkwebview中的cookie 方法1
    for (NSHTTPCookie *cookie in cookies) {
//        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSLog(@"wkwebview中的cookie:%@", cookie);

}
//读取wkwebview中的cookie 方法2 读取Set-Cookie字段
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
NSLog(@"wkwebview中的cookie:%@", cookieString);

//看看存入到了NSHTTPCookieStorage了没有
NSHTTPCookieStorage *cookieJar2 = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieJar2.cookies) {
NSLog(@"NSHTTPCookieStorage中的cookie%@", cookie);
}
decisionHandler(WKNavigationResponsePolicyAllow);
}

• (2)通过调用js的方法获取 cookie。

(void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{

[webView evaluateJavaScript:[NSString stringWithFormat:@"document.cookie"] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
if (response != 0) {
NSLog(@"\n\n\n\n\n\n document.cookie%@,%@",response,error);
}
}];
}

第2种方法获取的cookie并不全,所以我在项目中并不是通过这个获取的。

只能通过第1种方式去获取cookie,其实主要还是JSESSIONID,只要把JSESSIONID获取并保存了,下次打开app时就能用JSESSIONID来重新连接服务器的会话。

2、注入Cookie

• 注入 Cookie 就是从之前保存 cookie 的 NSHTTPCookieStorage 中取出相关 Cookie,然后在再次请求访问的时候在 request 中注入 Cookie。注入Cookie同样有多种方式。

• (1)JS注入1

//取出 storage 中的cookie并将其拼接成正确的形式
NSArray<NSHTTPCookie *> *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];    
NSMutableString *jscode_Cookie = [@"" mutableCopy];
[tmp enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@   =  %@", obj.name, obj.value);
[jscode_Cookie appendString:[NSString stringWithFormat:@"document.cookie =  '%@=%@';", obj.name, obj.value]];
}];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];

WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: jscode_Cookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
[requestObj setHTTPShouldHandleCookies:NO];
[_webView loadRequest:requestObj];

• (2)JS注入2

(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [webView evaluateJavaScript:@"document.cookie ='TeskCookieKey1=TeskCookieValue1';" completionHandler:^(id result, NSError *error) {
    //...
    }];
}

• (3) NSMutableURLRequest 注入Cookie

NSURL *url = request.URL;
NSMutableString *cookies = [NSMutableString string];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];

NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSDictionary *dicCookies = [NSHTTPCookie requestHeaderFieldsWithCookies:tmp];
NSString *cookie = [self readCurrentCookie];
[requestObj setValue:cookie forHTTPHeaderField:@"Cookie"];
[_webView loadRequest:requestObj];


-(NSString *)readCurrentCookie{
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}

// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
  NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
return cookieValue;
}

js注入这三种方式,亲测,只有第一种方式是成功的,而且requestObj setHTTPShouldHandleCookies:NO;这句话非常重要,意思是设置request的cookie是否跟随request请求发送和存储给默认的cookie管理。因为是在请求之前要将sessionId注入给浏览器然后发送给服务器,那么就要设置为NO,设置为NO的话,那浏览器默认打开时生成的cookie(sessionid)将不会跟随web一起发送给服务器,而通过NSMutableURLRequest带注入的cookie发送给服务器,同时将cookie设置到cookie管理器。

AFNetWork获取cookie

由于项目中用到了AFNetWork调取服务器接口,而且首次调用就会生成session,所以我想让AFNetWork生成的cookie与wkwebview共享,则只要从AFNetWork获取sessionid,然后再通过wkwebview调用服务器页面时,将session注入进去然后带给服务器就可以了。

通过如下代码获取cookie:
1,从cookie管理器中获取cookie(AFNetwork已经将cookie存入了标准的cookie管理器中,而wkwebview是不会存在标准的Http cookie管理中)

-(void)saveSession:(NSURL *)url{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url];
NSLog(@"request cookies:%@",cookies);
for (NSHTTPCookie *cookie in cookies) {
if ([K_SESSIONID isEqualToString:cookie.name] ) {
[[NSUserDefaults standardUserDefaults] setValue:cookie.value forKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
}

2, 注入进去

通过上面第一种方式就行了。

Cordova集成保持cookie免登陆

  • WFWKWebViewEngine.h:

      import "CDVWKWebViewEngine.h"
      import "CDVWebViewEngineProtocol.h"
      
      @interface WFWKWebViewEngine : CDVWKWebViewEngine
      
      
      @end
    
  • WFWKWebViewEngine.m:

      //
      //  WFWKWebViewEngine.m
      //  wkwvtest
      //
      //  Created by sam.hu on 2017/6/3.
      //
      //
      
      import "WFWKWebViewEngine.h"
      
      import "CDVWKProcessPoolFactory.h"
      import "WFSessionManager.h"
      
      @implementation WFWKWebViewEngine
      
      
      -(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
      
      [super webView:webView didStartProvisionalNavigation:navigation];
      
      
      
      }
      
      -(id)loadRequest:(NSURLRequest *)request{
      
      NSMutableURLRequest *req = [request mutableCopy];
      [req setHTTPShouldHandleCookies:NO];
      return [super loadRequest:req];
      }
      
      
      -(void)pluginInitialize{
      
      [super pluginInitialize];
      
      NSString *cookieValue= [[WFSessionManager instanceManager] readCurrentCookie];
      if (cookieValue) {
      //添加在js中操作的对象名称,通过该对象来向web view发送消息
      WKWebView *webView = (WKWebView *)self.engineWebView;
      WKUserScript * cookieScript = [[WKUserScript alloc]initWithSource:cookieValue   injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
      \[webView.configuration.userContentController addUserScript:cookieScript];
      webView.configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool];
      }
      }
      
      
      
      -(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
      
      decisionHandler(WKNavigationResponsePolicyAllow);
      }
      
      
      
      
      
      (void)webView:(WKWebView *)webView didFinishNavigation:(null\_unspecified WKNavigation *)navigation{
      
      [super webView:webView didFinishNavigation:navigation];
      
      
      [self.commandDelegate evalJs:@"getAppType('isApp','ios')"];
      
      
      }
      
      
      @end
    

session管理工具:

import <Foundation/Foundation.h>

@interface WFSessionManager : NSObject


+(instancetype)instanceManager;


-(NSString *)readCurrentCookie;

-(void)saveSession:(NSURL *)url;

-(void)reset;

@end

WFSessionManager.m :

import "WFSessionManager.h"



define  K_SESSIONID    @"JSESSIONID"


@implementation WFSessionManager




+(instancetype)instanceManager{

static dispatch_once_t onceToken;
static WFSessionManager * manager = nil;
dispatch_once(&onceToken, ^{
manager = [[[self class] alloc] init];
});
return manager;
}




-(NSString *)readCurrentCookie{

NSString* sessionId = [[NSUserDefaults standardUserDefaults] valueForKey:K_SESSIONID];
NSLog(@"cookie dictionary found is %@",sessionId);
if (sessionId) {
NSString *jssessionCookie = [NSString stringWithFormat:@"document.cookie = '%@=%@';", K_SESSIONID, sessionId];
return jssessionCookie;
}
return nil;
}

-(void)saveSession:(NSURL *)url{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url];
NSLog(@"request cookies:%@",cookies);
for (NSHTTPCookie *cookie in cookies) {
if ([K_SESSIONID isEqualToString:cookie.name] ) {
[[NSUserDefaults standardUserDefaults] setValue:cookie.value forKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
}

-(void)reset {

[[NSUserDefaults standardUserDefaults] removeObjectForKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
}

@end

wk做离线缓存

wkwebview本身不支持缓存,也不支持对资源文件的拦截。

网络辟谣

在做wkwebview离线缓存时,在网上看到有人说wkwebview直接支持cache,说什么只要设置:request setCachePolicy:NSURLRequestReturnCacheDataDontLoad;这样的缓存策略就行了,但我告诉你,这是瞎扯,说这种话的人都不懂。

webview怎么拦截

  1. webview是通过注册自定义的NSURLProtocol来做资源文件拦截
  2. 有人做测试发现wkwebview其实也是留了这个口子,只是没有实现类而已
  3. 使用了WKBrowsingContextController和registerSchemeForCustomProtocol。 通过反射的方式拿到了私有的 class/selector。通过kvc取到browsingContextController。通过把注册把 http 和 https 请求交给 NSURLProtocol 处理。

具体参考我的github:[demo][3]

ajax请求post的body丢失问题

  1. 如果post的大部分body内部不是特别多,可以用request的head的方式解决
  2. 但对于大的body数据,单纯用head的方式是无法解决的

对于第一种情况我现在不讨论,只说第二种情况,让wkwebview完美的支持post所有请求:

  • 将body在ajax之前通过与原生通信,将body传给app
  • app拿到body后先存到内存
  • ajax请求后,在截取的request中,将body拿出设置进去

html5端执行如下代码发送body的json数据给app:
window.webkit.messageHandlers.ajaxToWKWeb.postMessage({body: ‘hello world!’,reqkey:’请求随机数’});

其中ajaxToWKWeb是ios端wkwebview注册的方法。

注册代码如下:

config.userContentController = [[WKUserContentController alloc] init];

// 注入JS对象名称ajaxToWKWeb,当JS通过ajaxToWKWeb来调用时,我们可以在WKScriptMessageHandler代理中接收到
[config.userContentController addScriptMessageHandler:self name:@"ajaxToWKWeb"];

pragma mark - WKScriptMessageHandler
(void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message {
  if ([message.name isEqualToString:@"ajaxToWKWeb"]) {
// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
// NSDictionary, and NSNull类型
//do something
NSLog(@"%@", message.body);
  }
}

这种方式传输的数据内容大小限制为2M左右,如果有超过的将会被截掉,如果非要一次性传输这么大的数据量,可以分段传输。

[3]: https://github.com/huguiqi/NSURLProtocol-WebKitSupport “demo”

文章目录
  1. 1. WKWebView概述
  2. 2. • WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势1. 更多的支持HTML5的特性2. 高达60fps的滚动刷新率以及内置手势3. 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议4. Safari相同的JS引擎5. 占用更少的内存
  3. 3. 初步解决方案
    1. 3.1. 1、获取Cookie
    2. 3.2. 2、注入Cookie
  4. 4. AFNetWork获取cookie
  5. 5. Cordova集成保持cookie免登陆
  6. 6. wk做离线缓存
  7. 7. 网络辟谣
  8. 8. webview怎么拦截
  9. 9. ajax请求post的body丢失问题