WKWebView 请求拦截、加载本地资源、JS交互

2,143 阅读3分钟

背景

最近在工作中,遇到了这样的场景:使用WebView加载一个网页,网页需要用到公司一个公共框架下的css、js文件,如是便接触了WKWebView请求拦截、重定向加载本地资源文件、JS注入,记录一下。

调研

研究了业内已有的 WKWebView 请求拦截方案,主要分为如下两种:

NSURLProtocol

NSURLProtocol 默认会拦截所有经过 URL Loading System 的请求,因此只要 WKWebView 发出的请求经过 URL Loading System 就可以被拦截。

WKURLSchemeHandler

WKURLSchemeHandler 是 iOS 11 引入的新特性,负责自定义请求的数据管理,如果需要支持 scheme 为 http 或 https请求的数据管理则需要 hook WKWebView 的 handlesURLScheme: 方法,然后返回NO即可。

@implementation WKWebView (BMURLScheme)

+ (BOOL)handlesURLScheme:(NSString *)urlScheme
{
    return NO;
}

@end

对比:

  • 隔离性:NSURLProtocol 一经注册就是全局开启。一般来讲我们只会拦截自己的业务页面,但使用了 NSURLProtocol 的方式后会导致应用内合作的三方页面也会被拦截从而被污染。WKURLSchemeHandler 则可以以页面为维度进行隔离,因为是跟随着 WKWebViewConfiguration 进行配置。
  • 稳定性:NSURLProtocol 拦截过程中会丢失 Body,WKURLSchemeHandler 在 iOS 11.3 之前 (不包含) 也会丢失 Body,在 iOS 11.3 以后 WebKit 做了优化只会丢失 Blob 类型数据。
  • 一致性:WKWebView 发出的请求被 NSURLProtocol 拦截后行为可能发生改变,比如想取消 video 标签的视频加载一般都是将资源地址 (src) 设置为空,但此时 stopLoading 方法却不会调用,相比而言 WKURLSchemeHandler 表现正常。

结论:WKURLSchemeHandler 在隔离性、稳定性、一致性上表现优于 NSURLProtocol,但是想在生产环境投入使用必须要解决 Body 丢失的问题。

请求拦截WKURLSchemeHandler:

//WKWebView配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

if (@available(iOS 11.0, *)) {

        BMURLSchemeHandler * urlSchemeHandler = [[BMURLSchemeHandler alloc] init];
        [config setURLSchemeHandler:urlSchemeHandler forURLScheme:@"https"];
        //[config setURLSchemeHandler:urlSchemeHandler forURLScheme:@"customer"];

    } else {
        // Fallback on earlier versions
    }

代理实现 WKURLSchemeHandler

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0))
{

    NSMutableURLRequest * nsRequest = [urlSchemeTask.request mutableCopy];
    NSString * url_absoluteString = nsRequest.URL.absoluteString;
    NSLog(@"url = %@",url_absoluteString);

    //临时处理图片
    if(([url_absoluteString containsString:@".png"] || [url_absoluteString containsString:@".jpg"] || [url_absoluteString containsString:@".gif"] || [url_absoluteString containsString:@".webp"])){

        [BMURLSchemeHandler loadImage:url_absoluteString Task:urlSchemeTask];
    }
    else{

        NSString *path = nsRequest.URL.absoluteString;
        NSString * mimeType = @"";
        NSData * data = [NSData new];

        if ([path containsString:@"xx.htm"]) {
            mimeType = @"text/html";
            NSString *path = [[NSBundle mainBundle] pathForResource:@"xx.htm" ofType:nil];
            data = [NSData dataWithContentsOfFile:path];

        }else if ([path containsString:@"yy.css"]){
            mimeType = @"text/css";
            NSString *path = [[NSBundle mainBundle] pathForResource:@"yy.css" ofType:nil];
            data = [NSData dataWithContentsOfFile:path];

        }else if ([path containsString:@"zz.js"]){
            mimeType = @"application/x-javascript";
            NSString *path = [[NSBundle mainBundle] pathForResource:@"zz.js" ofType:nil];
            data = [NSData dataWithContentsOfFile:path];

        }

        NSMutableDictionary *resHeader = [NSMutableDictionary new];
        //跨域
        [resHeader setValue:@"*" forKey:@"Access-Control-Allow-Origin"];
        //中文乱码处理
        [resHeader setValue:@"charset=UTF-8" forKey:@"Content-Type"];
        //文件类型 html、css、js
        [resHeader setValue:mimeType forKey:@"Content-Type"];

        NSHTTPURLResponse *response =[[NSHTTPURLResponse alloc] initWithURL:nsRequest.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:resHeader];

        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];
    }
    
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0));
{
    NSLog(@"Stop❕❕❕ = %@",urlSchemeTask);
}


 //临时使用,可更换为SDWebImage框架 或者在这个方法上面加上缓存,加载过的图片存储下来,下次直接使用
+ (void)loadImage:(NSString*)url Task:(id <WKURLSchemeTask>)urlSchemeTask{

    NSURL *imgURL = [NSURL URLWithString:url];

    NSURLRequest *request = [NSURLRequest requestWithURL:imgURL];

    NSURLSessionDataTask * task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        NSHTTPURLResponse * hRes = (NSHTTPURLResponse *)response;

        if (hRes.statusCode == 200 ) {
            if (hRes.expectedContentLength == 0) {
                NSLog(@"Download image with empty content");
            }else{
                [urlSchemeTask didReceiveResponse:response];
                [urlSchemeTask didReceiveData:data];
                [urlSchemeTask didFinish];
            }
        }else{
            NSLog(@"Can not download image");
        }

    }];

    [task resume];
}

JS交互实现流程

1、JS、OC相互调用方法

  • JS调iOS

JS端使用window.webkit.messageHandlers.JS_FunctionName.postMessage(null),其中JS_FunctionName是iOS端提供个JS交互的Name。

function** jsCalliOS() {
    obj = {'msg':'JS TO OC'}    
    window.webkit.messageHandlers.jsToOc.postMessage(obj);
}
  • iOS调用js

[webView evaluateJavaScript:@"iOSCalljs('iOS Call Js')" completionHandler:^(**id** **_Nullable** response, NSError * **_Nullable** error) {

}];

// js
function iOSCalljs(str) {
     alert(str)
}

2、JS、原生交互实现

  • Webview初始化的时候添加WKScriptMessageHandler,主要用来做native与JavaScript的交互管理,

    WKUserContentController * wkUserContent= [[WKUserContentController alloc] init];

    self.scriptMessage = [[BMScriptMessageHandler alloc]init];

    [wkUserContent addScriptMessageHandler:self.scriptMessage name:@"jsToOC"];
  • 实现WKScriptMessageHandler代理:- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;{
    NSString * msgName = message.name;

    NSString * body = [NSString stringWithFormat:@"%@",message.body];

    if ([msgName isEqualToString:@"jsToOC"]) {
        NSLog(@"%@",body);// js传递给原生的数据
    }
}
  • js发起调用,传递数据
    //调用iOS端的JS_FunctionName
    window.webkit.messageHandlers.jsToOC.postMessage({body: 'js传递给原生的数据'});
  • 移除
//KVO等 移除

- (void)dealloc{
    [self.mWebView.configuration.userContentController removeScriptMessageHandlerForName:@"jsToOC"];
    self.scriptMessage = nil;
}

ps:刚开始写文章,写的很垃圾,不过也算跨出了第一步,以后慢慢进步。