wkwebview離線化載入h5資源解決方案

MeloAlright發表於2018-04-21

思路: 使用NSURLProtocol攔截請求轉發到本地。

本文作者為 簡書-melo的微博 / 掘金-melo的微博 / Github-meloalright,轉載請註明出處哦。

1.確認離線化需求

部門負責的app有一部分使用的線上h5頁,長期以來載入略慢... 

於是考慮使用離線化載入。
   
確保[低速網路]或[無網路]可網頁秒開。
複製程式碼

2.使用[NSURLProtocol]攔截

區別於uiwebview wkwebview使用如下方法攔截

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 區別於uiwebview wkwebview使用如下方法攔截
    Class cls = NSClassFromString(@"WKBrowsingContextController");
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    if ([(id)cls respondsToSelector:sel]) {
        [(id)cls performSelector:sel withObject:@"http"];
        [(id)cls performSelector:sel withObject:@"https"];
    }
}
複製程式碼
# 註冊NSURLProtocol攔截
- (IBAction)regist:(id)sender {
    [NSURLProtocol registerClass:[FilteredProtocol class]];
}
複製程式碼
# 登出NSURLProtocol攔截
- (IBAction)unregist:(id)sender {
    [NSURLProtocol unregisterClass:[FilteredProtocol class]];
}
複製程式碼

3.下載[zip] + 使用[SSZipArchive]解壓

需要先 #import "SSZipArchive.h

- (void)downloadZip {
    NSDictionary *_headers;
    NSURLSession *_session = [self sessionWithHeaders:_headers];
    NSURL *url = [NSURL URLWithString: @"http://10.2.138.225:3238/dist.zip"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 初始化cachepath
    NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSFileManager *fm = [NSFileManager defaultManager];
    
    // 刪除之前已有的檔案
    [fm removeItemAtPath:[cachePath stringByAppendingPathComponent:@"dist.zip"] error:nil];
    
    NSURLSessionDownloadTask *downloadTask=[_session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        if (!error) {
            
            NSError *saveError;
            
            NSURL *saveUrl = [NSURL fileURLWithPath: [cachePath stringByAppendingPathComponent:@"dist.zip"]];
            
            // location是下載後的臨時儲存路徑,需要將它移動到需要儲存的位置
            [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError];
            if (!saveError) {
                NSLog(@"task ok");
                if([SSZipArchive unzipFileAtPath:
                    [cachePath stringByAppendingPathComponent:@"dist.zip"]
                                   toDestination:cachePath]) {
                    NSLog(@"unzip ok");// 解壓成功
                }
                else {
                    NSLog(@"unzip err");// 解壓失敗
                }
            }
            else {
                NSLog(@"task err");
            }
        }
        else {
            NSLog(@"error is :%@", error.localizedDescription);
        }
    }];
    
    [downloadTask resume];
}
複製程式碼

4.遷移資源至[NSTemporary]

[wkwebview]真機不支援直接載入[NSCache]資源
需要先遷移資源至[NSTemporary]

- (void)migrateDistToTempory {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *cacheFilePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"dist"];
    NSString *tmpFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"dist"];
    
    // 先刪除tempory已有的dist資源
    [fm removeItemAtPath:tmpFilePath error:nil];
    NSError *saveError;
    
    // 從caches拷貝dist到tempory臨時資料夾
    [[NSFileManager defaultManager] copyItemAtURL:[NSURL fileURLWithPath:cacheFilePath] toURL:[NSURL fileURLWithPath:tmpFilePath] error:&saveError];
    NSLog(@"Migrate dist to tempory ok");
}
複製程式碼

5.轉發請求

如果[/static]開頭 => 則轉發[Request]到本地[.css/.js]資源
如果[index.html]結尾 => 就直接[Load]本地[index.html] (否則[index.html]可能會載入失敗)

//
//  ProtocolCustom.m
//  proxy-browser
//
//  Created by melo的微博 on 2018/4/8.
//  Copyright © 2018年 com. All rights reserved.
//
#import <objc/runtime.h>
#import <Foundation/Foundation.h>
#import <MobileCoreServices/MobileCoreServices.h>

static NSString*const matchingPrefix = @"http://10.2.138.225:3233/static/";
static NSString*const regPrefix = @"http://10.2.138.225:3233";
static NSString*const FilteredKey = @"FilteredKey";


@interface FilteredProtocol : NSURLProtocol
@property (nonatomic, strong) NSMutableData   *responseData;
@property (nonatomic, strong) NSURLConnection *connection;
@end
複製程式碼
@implementation FilteredProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [NSURLProtocol propertyForKey:FilteredKey inRequest:request]== nil;
}
複製程式碼
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSLog(@"Got it request.URL.absoluteString = %@",request.URL.absoluteString);

    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    //擷取重定向
    if ([request.URL.absoluteString hasPrefix:matchingPrefix])
    {
        NSURL* proxyURL = [NSURL URLWithString:[FilteredProtocol generateProxyPath: request.URL.absoluteString]];
        NSLog(@"Proxy to = %@", proxyURL);
        mutableReqeust = [NSMutableURLRequest requestWithURL: proxyURL];
    }
    return mutableReqeust;
}
複製程式碼
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
    return [super requestIsCacheEquivalent:a toRequest:b];
}
複製程式碼
# 如果[index.html]結尾 => 就直接[Load]本地[index.html]
- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    // 標示改request已經處理過了,防止無限迴圈
    [NSURLProtocol setProperty:@YES forKey:FilteredKey inRequest:mutableReqeust];
    
    if ([self.request.URL.absoluteString hasSuffix:@"index.html"]) {

        NSURL *url = self.request.URL;
 
        NSString *path = [FilteredProtocol generateDateReadPath: self.request.URL.absoluteString];
        
        NSLog(@"Read data from path = %@", path);
        NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:path];
        NSData *data = [file readDataToEndOfFile];
        NSLog(@"Got data = %@", data);
        [file closeFile];
        
        //3.拼接響應Response
        NSInteger dataLength = data.length;
        NSString *mimeType = [self getMIMETypeWithCAPIAtFilePath:path];
        NSString *httpVersion = @"HTTP/1.1";
        NSHTTPURLResponse *response = nil;
        
        if (dataLength > 0) {
            response = [self jointResponseWithData:data dataLength:dataLength mimeType:mimeType requestUrl:url statusCode:200 httpVersion:httpVersion];
        } else {
            response = [self jointResponseWithData:[@"404" dataUsingEncoding:NSUTF8StringEncoding] dataLength:3 mimeType:mimeType requestUrl:url statusCode:404 httpVersion:httpVersion];
        }
        
        //4.響應
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
    }
    else {
        self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
    }
}
複製程式碼
- (void)stopLoading
{
    if (self.connection != nil)
    {
        [self.connection cancel];
        self.connection = nil;
    }
}
複製程式碼
- (NSString *)getMIMETypeWithCAPIAtFilePath:(NSString *)path
{
    if (![[[NSFileManager alloc] init] fileExistsAtPath:path]) {
        return nil;
    }
    
    CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
    CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass (UTI, kUTTagClassMIMEType);
    CFRelease(UTI);
    if (!MIMEType) {
        return @"application/octet-stream";
    }
    return (__bridge NSString *)(MIMEType);
}
複製程式碼
#pragma mark - 拼接響應Response
- (NSHTTPURLResponse *)jointResponseWithData:(NSData *)data dataLength:(NSInteger)dataLength mimeType:(NSString *)mimeType requestUrl:(NSURL *)requestUrl statusCode:(NSInteger)statusCode httpVersion:(NSString *)httpVersion
{
    NSDictionary *dict = @{@"Content-type":mimeType,
                           @"Content-length":[NSString stringWithFormat:@"%ld",dataLength]};
    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:requestUrl statusCode:statusCode HTTPVersion:httpVersion headerFields:dict];
    return response;
}
複製程式碼
#pragma mark- NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}
複製程式碼
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.responseData = [[NSMutableData alloc] init];
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
複製程式碼
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.responseData appendData:data];
    [self.client URLProtocol:self didLoadData:data];
}
複製程式碼
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}
複製程式碼
+ (NSString *)generateProxyPath:(NSString *) absoluteURL {
    NSString *tmpFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"dist"];
    NSString *fileAbsoluteURL = [@"file:/" stringByAppendingString:tmpFilePath];
    return [absoluteURL stringByReplacingOccurrencesOfString:regPrefix
                                                 withString:fileAbsoluteURL];
}
複製程式碼
+ (NSString *)generateDateReadPath:(NSString *) absoluteURL {
    NSString *fileDataReadURL = [NSTemporaryDirectory() stringByAppendingPathComponent:@"dist"];
    return [absoluteURL stringByReplacingOccurrencesOfString:regPrefix
                                                  withString:fileDataReadURL];
}
@end
複製程式碼

結語:

完整[DEMO]請參考: github.com/meloalright…
(∩_∩)求給個☆哦

鳴謝:

參考文件:
1.簡書: iOS UIWebView小整理(三)(利用NSURLProtocol載入本地js、css資源)
2.Github: Yeatse/NSURLProtoc…
3.簡書: 獲得檔案的MIME Type

相關文章