WKWebView的请求拦截以及离线加载方案,使用WKURLSchemeHandler

背景说明

1、为了在线上能够及时的更新H5表现以及紧急修复一些问题,类似热更新的功能
2、为了Web端的开发人员可以实时的调试(修改本地host)

使用WKURLSchemeHandler而不是NSURLProtocol来拦截
参考文档:https://zhuanlan.zhihu.com/p/56965133

herald-hybrid/myhybrid/只是示例,你可以自定义
/myhybrid/是你跟网页约定的字段,包含此字段的html网页是需要拦截的,同时在缓存到本地时也是一个关键字段
herald-hybrid是URL Scheme,是你跟webView约定需要拦截的协议

一、设置URL Scheme

在创建webView设置configuration之前,给configuration注册handle

// 初始化 webViewConfiguration
WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init];

// 需要通过 webViewConfiguration 注册
[webViewConfiguration setURLSchemeHandler:[[MyURLSchemeHandler alloc] init] forURLScheme:@"herald-hybrid"];
// MyURLSchemeHandler 是我的项目中实现的 WKURLSchemeHandler,下文详述如何实现

// ...其他配置
// 初始化 WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration];

经过以上过程,当前 WKWebView 中所有的 herald-hybrid:// 协议均由 MyURLSchemeHandler 接管。

二、将webView的请求修改为herald-hybrid开头

例如你跟后台约定,在url中包含/myhybrid/字段的请求,都需要拦截
那你就需要判断你的url是否包含该字段,然后替换http开头为herald-hybrid,这样你的其他请求就不会被拦截

NSString *urlStr = @"http://hybrid.myseu.cn/myhybrid/index.html";

if ([urlStr containsString:MOJI_URL_WORD_KEY]) {
    urlStr = [urlStr stringByReplacingOccurrencesOfString:@"http" withString:MOJI_URL_SCHEME_KEY];
}

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]]];

三、准备工作做完了,开始主要内容【处理拦截】

先创建继承自WKURLSchemeHandler的子类MyURLSchemeHandler

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

extern NSString * _Nonnull const MOJI_URL_SCHEME_KEY;
extern NSString * _Nonnull const MOJI_URL_WORD_KEY;

NS_ASSUME_NONNULL_BEGIN

@interface MyURLSchemeHandler : NSObject<WKURLSchemeHandler>

+ (void)removeAllUrlCacheFile;

@end

NS_ASSUME_NONNULL_END

然后是在.m中的实现如下

#import "MyURLSchemeHandler.h"

NSString *const MOJI_URL_SCHEME_KEY = @"herald-hybrid";
NSString *const MOJI_URL_WORD_KEY   = @"/myhybrid/";

@implementation MyURLSchemeHandler

- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
    NSURL *URL        = urlSchemeTask.request.URL;
    NSString *urlPath = URL.absoluteString;
    NSLog(@"拦截到请求的URL:%@", URL);
    
    if (![urlPath containsString:MOJI_URL_WORD_KEY]) return; // 链接中不包含我们的关键字,则return
    
    //1.确定正在请求的文件是哪一个
    NSString *localFileName = [URL lastPathComponent];
    NSLog(@"本地文件名称:%@", localFileName);
    
    //2.读取本地文件数据/信息
    NSString *localFilePath = [MyURLSchemeHandler urlCacheRootFilePath];
    
    NSArray *pathArr   = [urlPath componentsSeparatedByString:MOJI_URL_WORD_KEY];
    NSString *lastPath = [NSString stringWithFormat:@"%@%@", MOJI_URL_WORD_KEY, pathArr.lastObject];
    NSLog(@"从url中提取的路径:%@", lastPath);
    
    localFilePath = [NSString stringWithFormat:@"%@%@", localFilePath, lastPath];
    NSLog(@"拼接之后的本地文件路径:%@", localFilePath);
    
    //3.判断本地是否有该文件,有的话使用本地离线数据
    if ([[NSFileManager defaultManager] fileExistsAtPath:localFilePath]) {
        // 读取文件数据
        NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:localFilePath];
        NSData *data       = [file readDataToEndOfFile];
        [file closeFile];

        NSURLResponse *tmpResponse = [MyURLSchemeHandler getURLResponseWithURL:URL data:data];
        [urlSchemeTask didReceiveResponse:tmpResponse];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];

        NSLog(@"使用本地缓存文件来加载数据");
        return;
    }
    
    //4.本地没有离线数据的话,就进行url请求,记得将MOJI_URL_SCHEME_KEY改回http,不然无法请求,然后将请求的资源保存到本地
    NSString *tmpUrl = [URL.absoluteString stringByReplacingOccurrencesOfString:MOJI_URL_SCHEME_KEY withString:@"http"];
    NSURL *tmpURL    = [NSURL URLWithString:tmpUrl];

    NSLog(@"开始请求数据, url = %@", tmpURL);
    NSURLSession *session      = NSURLSession.sharedSession;
    NSURLSessionDataTask *task = [session dataTaskWithURL:tmpURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            /*
             【注意】:这里不能直接使用response返回,所以需要手动构建tmpResponse
              1、response可能为空, 或者其中的content-type是不准确的,例如css的content-type返回的是application/javascript,应该用text/css
              2、【非常重要】response的url是tmpURL,是http开头的,这样的话js中嵌套的js会以http来请求,就无法再通过startURLSchemeTask拦截了,
                要改成最开始拦截的那个URL才行,这样js中的js也会以MOJI_URL_SCHEME_KEY开头去请求,从而被拦截到
             */
            NSURLResponse *tmpResponse = [MyURLSchemeHandler getURLResponseWithURL:URL data:data];
            
            [urlSchemeTask didReceiveResponse:tmpResponse];
            [urlSchemeTask didReceiveData:data];
            [urlSchemeTask didFinish];
            
            // 能走到这里的,都是符合条件的,不过还是添加一层判断
            if ([tmpUrl containsString:MOJI_URL_WORD_KEY]) {
                NSString *tmpName         = [NSString stringWithFormat:@"/%@", localFileName];
                NSString *pathWithoutName = [localFilePath stringByReplacingOccurrencesOfString:tmpName withString:@""];
                
                if (![[NSFileManager defaultManager] fileExistsAtPath:pathWithoutName]) {
                    [[NSFileManager defaultManager] createDirectoryAtPath:pathWithoutName withIntermediateDirectories:YES attributes:nil error:nil];
                    NSLog(@"新建的本地文件路径:%@", pathWithoutName);
                }
                
                NSError *error = nil;
                [data writeToFile:localFilePath options:0 error:&error];
                
                if (error) {
                    NSLog(@"新保存的本地文件,保存失败:%@", localFilePath);
                    NSLog(@"%s > failed for > %@", __PRETTY_FUNCTION__, error.localizedDescription);
                } else {
                    NSLog(@"新保存的本地文件,保存成功:%@", localFilePath);
                    NSLog(@"%s > done.", __PRETTY_FUNCTION__);
                }
            }
        } else {
            [urlSchemeTask didFailWithError:error];
        }
    }];
    [task resume];
}

- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
    // nothing
}

+ (NSURLResponse *)getURLResponseWithURL:(NSURL *)URL data:(NSData *)data {
    NSString *pathExtension = [URL pathExtension];
    NSString *MIMEType      = [MyURLSchemeHandler getMIMETypeFromPathExtension:pathExtension];
    return [[NSURLResponse alloc] initWithURL:URL MIMEType:MIMEType expectedContentLength:data.length textEncodingName:nil];
}

+ (NSString *)getMIMETypeFromPathExtension:(NSString *)pathExtension {
    NSString *MIMEType = @"text/plain";
    
    if ([pathExtension isEqualToString:@"html"]) {
        MIMEType = @"text/html";
    } else if ([pathExtension isEqualToString:@"js"]) {
        MIMEType = @"application/javascript";
    } else if ([pathExtension isEqualToString:@"css"]) {
        MIMEType = @"text/css";
    } else if ([pathExtension isEqualToString:@"png"]) {
        MIMEType = @"image/png";
    } else if ([pathExtension isEqualToString:@"jpeg"]) {
        MIMEType = @"image/jpeg";
    } else if ([pathExtension isEqualToString:@"json"]) {
        MIMEType = @"application/json";
    } else if ([pathExtension isEqualToString:@"xml"]) {
        MIMEType = @"application/xml";
    } else if ([pathExtension isEqualToString:@"pdf"]) {
        MIMEType = @"application/pdf";
    } else if ([pathExtension isEqualToString:@"gif"]) {
        MIMEType = @"image/gif";
    } else if ([pathExtension isEqualToString:@"mp3"]) {
        MIMEType = @"audio/mpeg";
    } else if ([pathExtension isEqualToString:@"mp4"]) {
        MIMEType = @"video/mp4";
    } else if ([pathExtension isEqualToString:@"zip"]) {
        MIMEType = @"application/zip";
    }
    
    return MIMEType;
}

+ (NSString *)urlCacheRootFilePath {
    NSString *filePath = [MyURLSchemeHandler documentsDirectoryPath];
    return [filePath stringByAppendingPathComponent:MOJI_URL_SCHEME_KEY];
}

+ (NSString *)documentsDirectoryPath {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
}

+ (void)removeAllUrlCacheFile {
    NSString *path = [MyURLSchemeHandler urlCacheRootFilePath];
    
    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
    }
}

@end

四、一些问题

注意事项基本都备注说明了
以上代码还有一个问题就是没有自动删除之前的缓存, 因为我们的内容都不会很多,每次加载新的可能都只有十几K,所以暂时没有处理之前的缓存。
不过因为留下了removeAllUrlCacheFile的方法,可以一键清除。

使用此方法加载离线数据,需要处理好版本控制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容