WKWebView
的優點這里不做過多介紹,主要說一下最近解決WKWebView
的post
請求丟失body問題的解決方案。
WKWebView
通過loadrequest
方法加載Post請求會丟失請求體(body)中的內容,進而導致服務器拿不到body中的內容的問題的發生。這個問題的產生主要是因為WKWebView
的網絡請求的進程與APP不是同一個進程,所以網絡請求的過程是這樣的:
由APP所在的進程發起request,然后通過IPC通信(進程間通信)將請求的相關信息(請求頭、請求行、請求體等)傳遞給webkit
網絡線進程接收包裝,進行數據的HTTP請求,最終再進行IPC的通信回傳給APP所在的進程的。這里如果發起的reques
t請求是post請求
的話,由于要進行IPC數據傳遞,傳遞的請求體body中根據系統調度,將其舍棄,最終在WKWebView
網絡進程接受的時候請求體body中的內容變成了空,導致此種情況下的服務器獲取不到請求體,導致問題的產生。
為了能夠獲取POST
方法請求之后的body內容
,這兩天整理了一些解決方案,大致分為三種:
1.將網絡請求交由Js發起,繞開系統
WKWebView
的網絡的進程請求達到正常請求的目的
2.改變POST請求
的方法為GET方法
(有風險,不一定服務器會接受GET方法)
3.將Post請求的請求body內容
放入請求的Header
中,并通過URLProtocol
攔截自定義協議,在攔截中通過NSConnection
進行重新請求(重新包裝請求body),然后通過回調Client
客戶端來傳遞數據內容
三種方法中,我采用了第三種方案,這里說一下第三種方案的實現方式,大致分為三步:
1.注冊攔截的自定義的scheme
2.重寫loadRequest()
方法,根據request
的method
方法是否為POST
進行URL
的攔截替換
3.在URLProtocol
中進行request
的重新包裝(獲取請求的body內容),使用NSURLConnection
進行HTTP
請求并將數據回傳
這里說明一下為什么要自己去注冊自定義的scheme
,而不是直接攔截https/http
。主要原因是:如果注冊了https/http
的攔截,那么所有的http(s)
請求都會交由系統進程處理,那么此時系統進程會通過IPC的形式傳遞給實現URLProctol
協議的類去處理,在通過IPC
傳遞的過程中丟失body
體(上面有講到),所以在攔截的時候是拿不到POST方法的請求體body的。然而并不是所有的http請求都會走loadrequest()
方法(比如js中的ajax請求),所以導致一些POST
請求沒有被包裝(將請求體body內容
放到請求頭header
)就被攔截了,進而丟失請求體body
內容,問題一樣會產生。所以為了避免這樣的問題,我們需要自己去定一個scheme協議
,保證不過度攔截并且能夠處理我們需要處理的POST
請求內容。
以下是具體的實現方式:
1.注冊攔截的自定義的
scheme
[NSURLProtocol registerClass:NSClassFromString(@“GCURLProtocol")];
[NSURLProtocol wk_registerScheme:@"gc"];
[NSURLProtocol wk_registerScheme:WkCustomHttp];
[NSURLProtocol wk_registerScheme:WkCustomHttps];
2.重寫
loadRequest()
方法,根據request
的method
方法是否為POST
進行URL
的攔截替換
//包裝請求頭內容
- (WKNavigation *)loadRequest:(NSURLRequest *)request{
NSLog(@"發起請求:%@ method:%@",request.URL.absoluteString,request.HTTPMethod);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
NSMutableDictionary *requestHeaders = [request.allHTTPHeaderFields mutableCopy];
//判斷是否是POST請求,POST請求需要包裝request中的body內容到請求頭中(會有丟失body問題的產生)
//,包裝完成之后重定向到攔截的協議中自己包裝處理請求數據內容,攔截協議是GCURLProtocol,請自行搜索
if ([mutableRequest.HTTPMethod isEqualToString:@"POST"] && ([mutableRequest.URL.scheme isEqualToString:@"http"] || [mutableRequest.URL.scheme isEqualToString:@"https"])) {
NSString *absoluteStr = mutableRequest.URL.absoluteString;
if ([[absoluteStr substringWithRange:NSMakeRange(absoluteStr.length-1, 1)] isEqualToString:@"/"]) {
absoluteStr = [absoluteStr stringByReplacingCharactersInRange:NSMakeRange(absoluteStr.length-1, 1) withString:@""];
}
if ([mutableRequest.URL.scheme isEqualToString:@"https"]) {
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"https" withString:WkCustomHttps];
}else{
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"http" withString:WkCustomHttp];
}
mutableRequest.URL = [NSURL URLWithString:absoluteStr];
NSString *bodyDataStr = [[NSString alloc]initWithData:mutableRequest.HTTPBody encoding:NSUTF8StringEncoding];
[requestHeaders addEntriesFromDictionary:@{@"httpbody":bodyDataStr}];
mutableRequest.allHTTPHeaderFields = requestHeaders;
NSLog(@"當前請求為POST請求Header:%@",mutableRequest.allHTTPHeaderFields);
}
return [super loadRequest:mutableRequest];
}
3.在
URLProtocol
中進行request
的重新包裝(獲取請求的body
內容),使用NSURLConnection
進行HTTP請求并將數據回傳(以下是主要代碼)
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString *scheme = request.URL.scheme;
if ([scheme isEqualToString:InterceptionSchemeKey]){
if ([self propertyForKey:HaveDealRequest inRequest:request]) {
NSLog(@"已經處理,放行");
return NO;
}
return YES;
}
if ([scheme isEqualToString:WkCustomHttp]){
if ([self propertyForKey:HaveDealWkHttpPostBody inRequest:request]) {
NSLog(@"已經處理,放行");
return NO;
}
return YES;
}
if ([scheme isEqualToString:WkCustomHttps]){
if ([self propertyForKey:HaveDealWkHttpsPostBody inRequest:request]) {
NSLog(@"已經處理,放行");
return NO;
}
return YES;
}
return NO;
}
- (void)startLoading {
//截獲 gc 鏈接的所有請求,替換成本地資源或者線上資源
if ([self.request.URL.scheme isEqualToString:InterceptionSchemeKey]) {
[self htmlCacheRequstLoad];
}
else if ([self.request.URL.scheme isEqualToString:WkCustomHttp] || [self.request.URL.scheme isEqualToString:WkCustomHttps]){
[self postBodyAddLoad];
}
else{
NSMutableURLRequest *newRequest = [self cloneRequest:self.request];
NSString *urlString = newRequest.URL.absoluteString;
[self addHttpPostBody:newRequest];
[NSURLProtocol setProperty:@YES forKey:GCProtocolKey inRequest:newRequest];
[self sendRequest:newRequest];
}
}
- (void)addHttpPostBody:(NSMutableURLRequest *)redirectRequest{
//判斷當前的請求是否是Post請求
if ([self.request.HTTPMethod isEqualToString:@"POST"]) {
NSLog(@"post請求");
NSMutableDictionary *headerDict = [redirectRequest.allHTTPHeaderFields mutableCopy];
NSString *body = headerDict[@"httpbody"]?:@"";
if (body.length) {
redirectRequest.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@"body:%@",body);
}
}
}
- (void)postBodyAddLoad{
NSMutableURLRequest *cloneRequest = [self cloneRequest:self.request];
if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttps]) {
cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttps withString:@"https"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpsPostBody inRequest:cloneRequest];
}else if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttp]){
cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttp withString:@"http"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpPostBody inRequest:cloneRequest];
}
//添加body內容
[self addHttpPostBody:cloneRequest];
NSLog(@"請求body添加完成:%@",[[NSString alloc]initWithData:cloneRequest.HTTPBody encoding:NSUTF8StringEncoding]);
[self sendRequest:cloneRequest];
}
//復制Request對象
- (NSMutableURLRequest *)cloneRequest:(NSURLRequest *)request
{
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:request.URL cachePolicy:request.cachePolicy timeoutInterval:request.timeoutInterval];
newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
[newRequest setValue:@"image/webp,image/*;q=0.8" forHTTPHeaderField:@"Accept"];
if (request.HTTPMethod) {
newRequest.HTTPMethod = request.HTTPMethod;
}
if (request.HTTPBodyStream) {
newRequest.HTTPBodyStream = request.HTTPBodyStream;
}
if (request.HTTPBody) {
newRequest.HTTPBody = request.HTTPBody;
}
newRequest.HTTPShouldUsePipelining = request.HTTPShouldUsePipelining;
newRequest.mainDocumentURL = request.mainDocumentURL;
newRequest.networkServiceType = request.networkServiceType;
return newRequest;
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
/**
* 收到服務器響應
*/
NSURLResponse *returnResponse = response;
[self.client URLProtocol:self didReceiveResponse:returnResponse cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
/**
* 接收數據
*/
if (!self.recData) {
self.recData = [NSMutableData new];
}
if (data) {
[self.recData appendData:data];
}
}
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response
{
/**
* 重定向
*/
if (response) {
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
/**
* 加載失敗
*/
[self.client URLProtocol:self didFailWithError:error];
}