一、资源拦截/映射
为了增强用户浏览 H5 页面的体验,减少页面白屏时间,实现 js、css、image 等资源文件,以及页面 html 文件的本地映射(非首次打开 wkwebview 本身有缓存机制,不包含 html 加载)。
1、资源拦截的过程
- web 端发起资源加载的请求(js、css、image)
- 使用 NSURLProtocol / WKURLSchemeHandler 实现资源请求的拦截
- 根据资源链接判读文件是否缓存于本地
- 匹配到有效的资源,读取文件后回传给 web 端
- 没有匹配到有效的资源,下载文件后回传给 web 端
WKWebView 需要注册 scheme 才能实现 URLProtocol 的拦截。
@implementation NSURLProtocol (WKWebKit)
/* WKWebView注册Scheme for URLProtocol;WKBrowsingContextController为私有API,可以通过Base64编码来绕过私有API的检查 */
+ (void)wkRegisterScheme:(NSString *)scheme {
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
}
@end
自定义 scheme 需要前端额外改变原有的 scheme,对原有工程的侵入性比较大。
[NSURLProtocol wkRegisterScheme:@"localScheme"];
WKWebView 注册 http / https 的 scheme 实现 URLProtocol 的拦截,会导致 Http 请求的 Body 和 Cookie 丢失。Hook Ajax 请求,通过 JSBridge 的方式提前将 HTTPBody 传给 Native 存储,避免HTTPBody 丢失的情况;Hook document.cookie 方法,将 cookie 信息同步于NSHTTPCookieStorage。
[NSURLProtocol wkRegisterScheme:@"http"];
[NSURLProtocol wkRegisterScheme:@"https"];
WKURLSchemeHandler 是 iOS11 之后才支持的 WK 资源拦截能力,WKURLSchemeHandler 的具体使用可参考博客:WKURLScheme资源拦截-分析应用、WKURLScheme资源拦截-细节处理。
拦截 Http 请求也存在 Cookie 和 Body 丢失的问题,处理方式与 URLProtocol 类似,WKURLSchemeHandler + AjaxHook 的具体应用可以参考 Git 源码- MKWebResources。
- (void)setURLSchemeWithConfiguration:(WKWebViewConfiguration *)configuration {
// 设置http、https的URLScheme
if (@available(iOS 11.0, *)) {
WFURLSchemeHandler *schemeHandler = [[WFURLSchemeHandler alloc] init]; // webView.configuration持有该实例
if (![configuration urlSchemeHandlerForURLScheme:@"http"] && ![configuration urlSchemeHandlerForURLScheme:@"https"]) {
[configuration setURLSchemeHandler:schemeHandler forURLScheme:@"http"];
[configuration setURLSchemeHandler:schemeHandler forURLScheme:@"https"];
}
}
}
URLScheme 设置为 http / https 会导致 crash,可以通过 Hook WKWebview 的 handlesURLScheme 方法来解决该问题。
@implementation WKWebView (URLSchemeHandler)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod([self class], @selector(handlesURLScheme:));
Method swizzledMethod = class_getClassMethod([self class], @selector(wfHandlesURLScheme:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
+ (BOOL)wfHandlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:@"https"]
|| [urlScheme isEqualToString:@"http"]) {
return NO;
} else {
return [self wfHandlesURLScheme:urlScheme];
}
}
@end
2、拦截/下发相关配置
1)资源拦截开关配置
{
"usable" : 1,
"resourceBlackList" : [ // 不使用映射的文件列表
"https://m.host.com/release/product/detail/res/js/899239823883.js",
"https://m.host.com/release/activity/detail/res/img/827328788782.jpg",
...
],
"suffixBlackList" : [ // 不做拦截的资源后缀
"docx",
"pdf",
...
],
}
2)资源包下发配置
根据沙盒 webFast 目录中的本地资源信息表,获取最新的资源包配置。资源包分位全量包与差分包,全量包是新增/覆盖已有的资源包,差分包则是在原有的资源压缩包的基础上,通过差分算法与 patch 包合成最新的全量包(如:线上最新资源包的版本为 2.6,本地资源包的版本号为 2.1 只需要下差分包,本地资源包的版本号为 1.1 则需要下全量包)。
/* 资源包下发配置详情 */
[{
"moduleName": "chat_module,模块名",
"resourceId": "123456,资源id",
"hitRule": "cres.host/chat_module,资源命中规则",
"usable": "是否使用,1:正常使用",
"version": "资源包版本号,1.1",
"url": "全量包下载地址",
"md5": "验签",
"patches" : [
{
"version": "1.0",
"url": "差分包下载地址",
"md5": "验签",
}
]
},
{
"moduleName": "shop_module,模块名",
"resourceId": "123457,资源id",
"hitRule": "cres.host/shop_module,资源命中规则",
"usable": "是否使用,0:删除已有资源",
"version": "资源包版本号,2.2",
"url": "全量包下载地址",
"md5": "验签",
"patches": []
}]
本地资源信息表:resourceId(资源id)、moduleName(模块名)、version(资源版本)、resourcePath(资源存储目录)、fullZipPath(全量包存储路劲)、 accessTime(最近访问时间)
资源命中规则:资源链接命中资源包的规则(资源链接 cres.host/chat_module/res/..utils.js,命中规则 cres.host/chat_module)
3)资源包更新策略
具体的资源包更新策略如下:
根据不同场景制定不同的异常重试机制,也可以默认不处理。
3、资源包目录结构
1)自定义资源包目录
大致结构图如下(webFast | module | js、css、image、font)
具体资源包的 file.json 内容格式如下:
{
"urlPath": {
"localPath" : "相对路劲",
"md5" : "文件验签,资源链接有带验签可以忽略"
}
}
/* 用文件url与hitRule匹配到资源包信息,通过resourcePath获取具体资源包的file.json */
本地资源文件的获取与校验:
- 用文件 url 与 hitRule 匹配到资源包信息
- 通过 resourcePath 获取具体资源包的 file.json
- 找到文件的 localPath,与 resourcePath 合成绝对路劲
- 获取资源文件数据,用 fileData 生成文件 md5 签名
- 两个 md5 值相比较,验证资源文件的有效性
2)以资源链接的Host/Path为目录结构
大致结构图如下(webFast | module | res | js | common | utils)
文件链接:http://cres.host/module_name/res/js/common/utils/stream.3e3d3ab3c2ab7412eb923d3ab3c2eb48.js
文件目录:沙盒/Documents/web_resource/cres.host/module_name/res/js/common/utils
- 储存的相对路径为 URL-Path,更贴近于前端资源的部署方式
- 不依赖于 file.json,直接通过 URL-Path 去映射,方式更加快捷
通过 URL-Path 获取文件的相对路劲,生成文件的沙盒路劲,多线程获取对应路径的资源文件,按照约定规则生成文件的 md5 签名,与 URL-Path 中的 md5 签名相比较,验证文件的有效性。
3)两种目录结构的对比
- 自定义的目录结构需要下发各个模块的文件路由表,支持 html 文件本地化
- Host/Path 的目录结构则通过资源链接生成本地文件的绝对路劲,不支持 html 文件本地化(html 无法验签)
二、html资源包本地化
类似于浏览器存储 H5 静态页面的方式,把页面所涉及到的资源文件打包,配置下方到沙盒目录,通过路由配置映射到页面对应的本地 html 文件,实现 H5 页面的渲染。最终得达到页面加速的效果,减少了页面白屏时间。
H5 资源包本地化实现更加简单,免去了 URLProtocol 拦截映射的流程,直接加载本地 html 文件,不过存在外部 CDN 资源请求跨域问题。
webview 的具体页面加载:
NSMutableURLRequest* mURLRequest = [NSMutableURLRequest requestWithURL:@"file://../Documents/web/moulde/html/index.html"];
[self.webView loadRequest:mURLRequest];
/* wkwebView默认不允许运行在一个URL环境中的JavaScript访问来自其他URL环境的内容,需要在wkwebView初始化中添加以下配置 */
[configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
html 中的资源文件 src 配置:
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=format-detection content="telephone=no"><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1,minimum-scale=1,maximum-scale=1,viewport-fit=cover"><title>pluto-h5</title>
<link rel="shortcut icon" href=../favicon.ico>
<link href=../css/app.6abe8eefcc9a65195cb591192f24cbe3.css rel=stylesheet></head><body><div id=app></div>
<script type=text/javascript src=../js/manifest.23dab50270570794ea32.js></script>
<script type=text/javascript src=../js/vendor.47866ead83e2f6d0de33.js></script>
<script type=text/javascript src=../js/app.9e1efd778583cb67d137.js></script>
</body></html>
file.json 用于对应 module 的文件映射:
{
"versionName": "1.0.0",
"pathMap": {
"js/vendor.47866ead83e2f6d0de33.js": "47a96eacd8259c4d882da5044ea4b36f"
},
"md5Map": {
"47a96eacd8259c4d882da5044ea4b36f": "js/vendor.47866ead83e2f6d0de33.js"
}
}
routes.json 用于匹配具体的页面 html 地址:
"map": {
"http://m.host.com/shopping/product/detail/index.html": "/web/module/product_detail/index.html",
"http://m.host.com/shopping/order/list/index.html": "/web/module/order_list/index.html"
}