由于当时公司的旧 Web 容器已无法持续保护(懂得都懂),所以需要重构一套新的来支撑越来越多的在线页面事务系统。但在阻拦资源,本地缓存加快这个过程中,踩了特别多的坑,这儿特别记载一下,让咱们们能少走些弯路。
布景
简略聊下布景,曾经事务上更多是运用 Web 离线包下载到本地,然后加载本地资源渲染,类似小程序相同的规划。但随之问题也许多,究竟没有一套成熟的开发系统,最重要的是开发的离线包都和盲盒相同,联调排查问题的时刻都赶的上开发时刻了。
在降本增效的条件下,已经不或许有人力单独为 App 开发一套离线包来支撑事务的状况下,必然就需要融合前端系统,直接加载在线页面。
那随之而来的一个结果便是对用户来说呈现了体验降级的状况,曾经秒开的页面变慢了,乃至在网络差的状况下白屏状况变得非常明显。
当然,这肯定是不能承受的,所以要重新建造整个 Web 容器。
总体规划
其他建造暂时不提,这儿只聊聊,怎么让在线 URL 页面到达秒开加载这件事。
方案选型
怎么提高秒开率?便是减少整个建立连接到渲染完成的这段时刻。
其实说白了,无非便是资源缓存加上提早预加载,而这也有几种办法能挑选。
WebView 自带 Cache
最常见的便是 WebView 自带缓存 Cache,缓存规则也是依托于前端开发,但这个缓存战略上常常会有问题,比方版别不对、意外白屏、缓存丢掉加载过慢等问题。
简略代码示意(来源 GPT-3.5)
// 装备缓存战略以及是否可运用cookie
WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
WKWebsiteDataStore *nonPersistentDataStore = [store nonPersistentDataStore];
WKWebsiteDataStore *ephermeralDataStore = [store ephemeralDataStore];
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
diskCapacity:500 * 1024 * 1024
diskPath:nil];
configuration.websiteDataStore = nonPersistentDataStore;
// 装备 URL 缓存战略
configuration.urlCache = cache;
configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
// 例如,设置一个自界说的Cookie战略
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
NSString *cookieScript = @"document.cookie = 'cookie_name=cookie_value; domain=your_domain.com;';";
WKUserScript *cookieScriptObj = [[WKUserScript alloc] initWithSource:cookieScript
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
运用前端方案service worker
service worker
是一种 Web API,它允许开发者将脚本文件运行在用户浏览器的后台进程中,独立于当时页面/标签,供应了强大的离线数据存储、网络恳求阻拦和消息推送等功用。
但其最大的硬伤是,在 iOS App 中是不被支撑的,这在苹果开发文档中是有清晰的。只在 iOS Safari 浏览器上才被支撑。
阻拦资源恳求
以上两种还有一个硬伤,便是有必要依托 WebView 环境,那在预加载上就有必要发动一个 WebView 容器,这一点其实有性能损耗的,假如有多个页面需要预缓存,那就有必要开多个容器或许供应一个预加载队列,控制起来也是尤为费事,特别在预加载完之前用户就进入页面的情景下,缓存意外也是有或许的。
那咱们还能怎么做呢?
其实思路很简略,在总体规划上也说的很明白。
咱们阻拦资源加载的恳求,让它去拜访咱们本地资源,假如本地资源不存在就先下载到本地再回来给 Web 页面。
那提早预加载,就让前端供应资源清单,咱们依据资源清单提早做一个资源更新即可。这样的好处还在于就算预加载未完成用户就进入了 Web 页面,那也不要紧,已下载好的资源仍然可以供应加快才能。
iOS 的坑
方案大致讲了下, Android 完成很顺利的就完成了。但 iOS 的确就被 WKWebView 这货卡住了。
想象真的很夸姣,但网上的资源乃至 GPT-3.5 给出的方案有真有假的,尝试了许多种,走了许多歪门邪道。
邪道一: NSURLProtocol
亲们,这个的确是不能用的,尽管咱们可以用它阻拦 http / https 恳求,也可以通过 + (BOOL)canInitWithRequest:(NSURLRequest *)request
办法来放过网络恳求,从 safari 调试下看 post 恳求内容也还在,但的确是在发送的时分就丢掉了 …
总结:仍是会阻拦掉网络恳求,而且丢掉了 post 恳求中的 body 信息。而且会影响整个全局。
邪道二: Hook XMLHttpRequest / fetch
这个是基于上一个“邪道”的延伸,已然恳求必定会被阻拦,那要不然就重写 XMLHttpRequest / fetch,让网络恳求走咱们的桥接办法,这样前端事务侧也无需修正任何代码。
代码示意(来源:反复问询 GPT-3.5 并修正)
function replaceXHR() {
// 保存原始的 XMLHttpRequest 结构函数
const origXHR = window.XMLHttpRequest
// 界说新的 XMLHttpRequest 结构函数
function NewXHR() {
const xhr = new origXHR()
let status = 200
let statusText = 'OK'
let response = ''
// 重写 open 办法
xhr.open = function (method, url, async, user, pass) {
this.url = url
this.method = method
origXHR.prototype.open.call(this, method, url, async, user, pass)
}
// 重写 send 办法
xhr.send = function (data) {
var urlObj
if (isUrlComplete(this.url)) {
urlObj = new URL(this.url)
} else {
urlObj = new URL('https://gaoding.com' + this.url)
}
const path = urlObj.pathname
const params = new URLSearchParams(urlObj.search)
// 非稿定恳求,不阻拦
if (!urlObj.host.includes('gaoding.com')) {
origXHR.prototype.send.call(this, data)
return
}
const paramObj = {}
for (const [key, value] of params) {
paramObj[key] = value
}
const config = {
method: this.method,
path: path,
query: paramObj,
}
if (this.method.toUpperCase() === 'POST' || this.method.toUpperCase() === 'PUT') {
config['body'] = data
}
// 执行桥接办法
request(config)
.then((response) => {
// 处理呼应结果
return response.result.response_data
})
.then((text) => {
// 调用原始的 onreadystatechange 函数,并传入呼应结果
this.status = 200
this.statusText = JSON.stringify(text)
this.response = text
this.onreadystatechange && this.onreadystatechange()
this.onload && this.onload()
})
.catch((error) => {
// 调用原始的 onerror 函数,并传入错误信息
this.onerror && this.onerror(error)
})
}
return xhr
}
// 用新的 XMLHttpRequest 结构函数替换原始的 XMLHttpRequest 结构函数
window.XMLHttpRequest = NewXHR
}
replaceXHR()
然后咱们在 WKWebView 中注入这段 JS 即可。
[userContentController addUserScript:[[WKUserScript alloc] initWithSource:[GDWebUtils injectJSForBundle:@"network-hook"] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]];
收效是收效了,但对咱们的 Web 页面却呈现了加载反常的状况,最终得知,咱们项目中已经对 XMLHttpRequest hook 过了,所以我这真的是魔改 …
总结:不能胡乱注入 JS 代码,简单翻车 …
邪道三:自界说 Scheme
为什么一开始会执着会执着去运用NSURLProtocol
,而不运用WKURLSchemeHandler
。原因是WKURLSchemeHandler
最低支撑 iOS 11, 且在 iOS 11.3 以下 post body 相同会丢掉[手动狗头]。
那笔者奇思妙想下,咱们先阻拦加载的 HTML,然后再替换其间的资源加载途径呢?
做法大约描绘下:
运用WKURLSchemeHandler
阻拦page://
和assets://
两种自界说的 Scheme。
比方加载https://www.google.com
时,其实是加载的 page://www.google.com
来让WKURLSchemeHandler
可以阻拦到页面加载了。
这样在 Response 中修正回来的资源加载途径,比方assets://xxxxx.css
,这样也就可以让WKURLSchemeHandler
阻拦到资源加载了。
理想很夸姣,关于页面来说的确阻拦加载成功了。
但
网络恳求跨域了 … 由于咱们当时域名是page://
而网络恳求发出去的是https://
。
真要这么做,只能是服务端开放跨域限制,但这一点关于服务端是极不安全的。
定论:绕了半天然并卵。
正道:阻拦 Http / Https Scheme
尽管苹果不主张乃至不允许阻拦 Http / Https 协议的 Scheme,但这便是唯一一种办法了。
运用条件
- 承认项目/功用只需支撑 iOS 11.3 以上版别。
- 承认前端其间没有File 上传恳求,由于就算 iOS 11.3 以上版别,也会丢掉 blob 格局的 body 数据。真要做文件上传,请供应给前端相应的 Bridge 办法。
- 切记处理 iOS 13 版别中的 post 恳求的崩溃问题(如下图)。
代码示意
让 Http / Https 恳求可被阻拦
黑魔法替换类办法完成
@implementation WKWebView (GDWebURLSchemeHandler)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod(self, @selector(handlesURLScheme:));
Method swizzledMethod = class_getClassMethod(self, @selector(gd_handlesURLScheme:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
+ (BOOL)gd_handlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:kGDWebHookURLScheme]) {
return NO;
} else {
return [self handlesURLScheme:urlScheme];
}
}
@end
阻拦步骤
结构阻拦类,完成WKURLSchemeHandler
协议
在 WKWebView 中注册
[config setURLSchemeHandler:[GDWebURLSchemeHandler new] forURLScheme:@"https"];
阻拦资源完成
上图中的GDWebAssetStorageService
服务便是做资源恳求阻拦及完成的。
内部完成也很简略,先判断本地是否存在资源,不存在就去下载后回来即可。
阻拦恳求完成
最终咱们仍是要处理阻拦到的网络恳求,WKURLSchemeHandler
协议真的是很坑爹,阻拦掉的就无法调用默认完成了,需要自己结构。
但从原理来讲,苹果这样规划是合理的,究竟 WKWebView 不是跟 App 同一个进程的,这牵扯到跨进程通讯的问题,也是为什么苹果会在恳求中过滤掉 blob 数据格局。
简略的结构一个NSURLSession
即可运用。
但这儿仍是有一个坑的,你会发现恳求重定向失效了,这儿咱们选用的是调用私有类来做一致的处理,私有类直接调用有审阅风险,简略的做一些代码混杂绕过去。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
// 调用 _didPerformRedirection:newRequest: 执行重定向
NSArray *privateSelStrArr = @[@"st:", @"que", @"ewRe", @"n:n", @"ctio", @"dire", @"ormRe", @"_didPerf"];
NSString *selName = [[[privateSelStrArr reverseObjectEnumerator] allObjects] componentsJoinedByString:@""];
SEL sel = NSSelectorFromString(selName);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.schemeTask performSelector:sel withObject:response withObject:request];
#pragma clang diagnostic pop
completionHandler(request);
}
定论
不想让前端代码做一些 App 个性化适配的条件下,想要提高秒开率,又不想开隐藏容器添加内存开支,那在 iOS 上只有这一种阻拦办法了。
尽管有着许多运用限制,但至少能满足现在的事务需求。
关于 blob
再简略的讲一下,关于前端 blob 数据传输给 App 的问题。
这个其实也走了许多歪门邪道,前期想着用 base64 的办法不如直接 blob 传输省性能,但的确是做不到的。
这儿也听了 GPT-3.5 的许多鬼话 … 要是用 GPT-4 的话,它会清晰告知你只有两种挑选:转 base64 或许 App 搭建本地服务器。AI 的差距真的非常明显[手动狗头]。
感谢阅览,假如对你有用请点个赞 ❤️
本文正在参加「金石方案」