WKWebview实现http拦截、下载缓存本地秒开,js<->Native带参交互,

主要实现两个功能

1.拦截http和https的请求并替换成缓存资源(兼容IOS10)
2.js<->Native的带参交互,参数可以直接是字典、数组、和字符串

1.拦截原理:

通过私有类WKBrowsingContextController 和让http和https执行私有API registerSchemeForCustomProtocol,
-(void)filtHTTP{
    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注册NSURLProtocol的派生类UrlRedirectionProtocol

  [NSURLProtocol registerClass:[UrlRedirectionProtocol class]];
//适当时机并卸载以防止泄漏和不需要拦截时依然被拦截
  [NSURLProtocol unregisterClass:[UrlRedirectionProtocol class]];

并在派生类实现url拦截并重定向本地url

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
//    NSLog(@"截获url : %@",request.URL.absoluteString);
    __block NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    //截取重定向
    [[UrlFiltManager shareInstance].urlFiltSet enumerateObjectsUsingBlock:^(NSString *host, BOOL * _Nonnull stop) {
        if ([request.URL.absoluteString hasPrefix:host])
        {
            NSURL* proxyURL = [NSURL URLWithString:[UrlRedirectionProtocol generateProxyPath: request.URL.absoluteString host:host]];
            //        NSLog(@"替换为url : %@",proxyURL.absoluteString);
            if ([[NSFileManager defaultManager]fileExistsAtPath:proxyURL.absoluteString]) {
                mutableReqeust = [NSMutableURLRequest requestWithURL: proxyURL];
                *stop = YES;
            }
        }

    }];
    return mutableReqeust;
}

重写startLoading方法实现本地资源重载

- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    // 标识该request已经处理过了,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:FilteredKey inRequest:mutableReqeust];
    if ([self checkNeedLoadingLocalData]) {
        __weak __typeof(&*self)weakSelf = self;
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __strong __typeof(&*weakSelf)strongSelf = weakSelf;
        NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:strongSelf.request.URL.absoluteString];
        NSData *data = [file readDataToEndOfFile];
        [file closeFile];
        //3.拼接响应Response
        NSInteger dataLength = data.length?:3;
        NSString *mimeType = [strongSelf getMIMETypeWithCAPIAtFilePath:strongSelf.request.URL.absoluteString];
        NSHTTPURLResponse *response = [strongSelf jointResponseWithData:data
                                                       dataLength:dataLength
                                                         mimeType:mimeType
                                                       requestUrl:strongSelf.request.URL
                                                       statusCode:dataLength?200:404
                                                      httpVersion:@"HTTP/1.1"];
        //4.响应
        [[strongSelf client] URLProtocol:strongSelf didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[strongSelf client] URLProtocol:strongSelf didLoadData:data];
        [[strongSelf client] URLProtocolDidFinishLoading:strongSelf];
        });
    }
    else {
        self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
    }
}

2.js交互原理:

通过向WKUserContentController的handler添加方法以供js调用
-(void)registOCMethods:(NSSet *)methods{
    for (NSString *method in methods) {
        [self.userContentController addScriptMessageHandler:self name:method];
    }
}
/*js调用oc方法通过回调执行,oc收到回调可以解析WKScriptMessage中的name(被js调用的oc方法名)和body(js传到oc消息提)并转换成oc方法
*/
#pragma mark -- WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    SEL selector =NSSelectorFromString([message.name stringByAppendingString:@":"]);
    IMP imp = [self methodForSelector:selector];
    void (*func)(id, SEL,id) = (void *)imp;
    if ([self respondsToSelector:selector]) {
        func(self, selector,message.body);
    }
}

js通过window.webkit.messageHandlers.方法名.postMessage(消息体)调用oc并传参

//js中的方法,js调oc的getAppEnv方法
function getAppInfo()
{
    //获取app的运行环境
    alert("获取appinfo");
    window.webkit.messageHandlers.getAppEnv.postMessage("");
}

//oc中的方法 js调oc 的getAppEnv方法
- (void)getAppEnv:(NSString *)env{
    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
    NSString *shareResult = [NSString stringWithFormat:@"appEnvResult('%@')",version];
    //OC调用JS返回结果
    [self.wkWebView evaluateJavaScript:shareResult completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@", error);
    }];
}

//js中的方法,js接收到oc调用appEnvResult方法和appEnv参数
function appEnvResult(appEnv)
{
    //获取到app的运行环境是 appEnv
    var content = "app回调数据" + appEnv;
    alert(content);
}

//oc调js通过wkwebview的api调用js

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

 [self.wkWebView evaluateJavaScript:@"js方法名('参数1','参数2','参数3',参数4)" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@", error);
}];

-(void)ocCallJs{
    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
    NSString *shareResult = [NSString stringWithFormat:@"appEnvResult('%@')",version];
    //OC调用JS
    [self.wkWebView evaluateJavaScript:shareResult completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@", error);
    }];
}

//oc调js传对象,可以讲字典和数组转换成json并替换"""成"'"传给js,js可以接受不了到的就直接是js对象了

//js中的方法 调用app分享并带一个字典参数
function callShare() {
    var shareInfo ={"title": "标题", "content": "内容", "shareUrl": "http://www.xxx.com","shareIco":"http://upload/xxxx"};
    //    Joy.share(shareInfo)
   window.webkit.messageHandlers.Share.postMessage(shareInfo);
}

//oc中的方法“share”被js调用传回参数dic字典对象
- (void)Share:(NSDictionary *)dic{
    if (![dic isKindOfClass:[NSDictionary class]]) {
        return;
    }
    NSString *title = [dic objectForKey:@"title"];
    NSString *content = [dic objectForKey:@"content"];
    NSString *url = [dic objectForKey:@"shareUrl"];
    //在这里写分享操作的代码
    NSLog(@"要分享了哦😯");
    //OC反馈给JS分享结果
    NSError * error = nil;
    NSData * jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    NSString * jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    
    NSString *replaceStr = [jsonStr stringByReplacingOccurrencesOfString:@"\"" withString:@"\'"];
    NSString *JSResult = [NSString stringWithFormat:@"shareResult('%@','%@','%@',%@)",title,content,url,replaceStr];
    //OC调用JS
    [self.wkWebView evaluateJavaScript:JSResult completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@", error);
    }];

//js接收oc方法并收到3个字符串参数和一个dic字典对象
function shareResult(channel_id,share_channel,share_url,dic) {
    //    var content = "app回调数据" + channel_id+","+share_channel+","+share_url;
    var content = dic['title'] + dic['content'] + dic['shareUrl'];
    alert(content);
    document.getElementById("shareResult").value = content;
}

我们可以把js调用oc和oc调用js的函数写成一个或多个category以实现不同模块的js<->native调用
比如我的wkwebview放到了一个WebVC的控制器里,然后把交互事件放到不同的category里,然后根据实际需要使用

@interface WebVC (JSNative)
@end

@implementation WebVC (JSNative)
- (void)Share:(NSDictionary *)dic{
}

- (void)ShareVideo:(NSDictionary *)dic{
}
@end
@interface WebVC (Media)
@end

@implementation WebVC (Media)
- (void)openAlbum:(NSDictionary *)dic{
}

- (void) openCamera:(NSDictionary *)dic{
}

- (void) openQRScan:(NSDictionary *)dic{
}
@end
测试,可以搭建一个简易服务器并把资源文件放进去以供下载解压使用,这里用python
cd /Users/joymake/Desktop/h5service 
//启动服务器,这里默认启动8000端口
python -m SimpleHTTPServer

//如果是模拟器地址用http://127.0.0.1:8000(demo里用的这个地址)就可以了,如果用的真机而资源包放在服务器上,那么地址需要改成服务器地址
ifconfig可用来查看当前服务器ip地址
启动本地服务.png

截图不知道为什么传上去后变这么大,知道的可以告诉我一下


demo文件结构
image.png
image.png
flutter资源

demo地址
https://github.com/joy-make/WKWebView.git

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

推荐阅读更多精彩内容