一个由不透明响应引发的灾难
当前页面是本站的「Baidu MIP」版。查看和发表评论请点击:完整版 »
不是应该只有 5MB 的缓存吗,怎么就“DOMException: Quota exceeded.”了?
Head Pic: #アズールレーン 湊 あくあ - 小小男爵不要坑的插画 - pixiv
跨域与不透明响应
不透明响应被 Service Worker 缓存简直就是个灾难
DOMException: Quota exceeded.
我的方舟工具箱一直没有给图片使用 CDN,原因是我在着迷 wiki 中看到的 360 图片 CDN 的域名p<x>.qhimg.com
(0<=x<=25)都是不支持 SSL 的,无法被 SW 缓存,所以一直使用项目中独立保存的图片,缓存和加载图片都十分缓慢,这一直是一个令我头疼的地方
后记:其实可以白嫖 jsDelivr 的……
而昨天下午我突发奇想搜索了下发现其实是有支持 SSL 的,域名为p<x>.ssl.qhimg.com
(0<=x<=6),这一下我就开心炸了,赶忙写了个 CDN 支持,并使用 Workbox 做运行时缓存
发布,测试,然后 BOOM,一气呵成
很怪,所有静态资源加起来也就 5MB 左右,怎么告诉我直接超额了?
全是不透明响应惹的祸
先打开缓存列表看看情况,但Content-Length
是正常的完全没有问题
经过一番思考(其实是走投无路没得选),我将目光放在了可疑的Response-Type: opaque
上
稍微搜索了下,在一则 Stack Overflow 问题 What limitations apply to opaque responses? 中找到了这段话
Opaque Responses & the navigator.storage API
In order to avoid leakage of cross-domain information, there's significant padding added to the size of an opaque response used for calculating storage quota limits (i.e. whether a
QuotaExceeded
exception is thrown) and reported by thenavigator.storage
API.The details of this padding vary from browser to browser, but for Google Chrome, this means that the minimum size that any single cached opaque response contributes to the overall storage usage is approximately 7 megabytes. You should keep this in mind when determining how many opaque responses you want to cache, since you could easily exceeded storage quota limitations much sooner than you'd otherwise expect based on the actual size of the opaque resources.
意思是,为了避免跨域信息泄漏,在计算储存空间时,不透明响应的大小会被添加大量填充,填充的大小因浏览器而异,对于 Chrome 来说,缓存单个不透明响应将至少占用约 7MB 空间
破案了,那这 100MB 空间确实不够存的
跟跨域请求讲道理
那么解决问题的关键就是如何让“不透明”响应变为“透明”响应了
再次观察之前的缓存列表,发现由 CSS 请求的字体文件为Response-Type: cors
,也就是说字体文件是正确发出了跨域请求的
对比字体文件的请求和其它请求,发现一个很关键的点,字体文件请求头带了origin
,而其它有问题的请求没有!
cors | opaque |
如果没有携带origin
头,CORS 就不会运作,所以我们得到了不透明响应
基于这一设想,我在 MDN 的一个文档 CORS settings attributes 中找到了这些描述
在HTML5中,一些 HTML 元素提供了对 CORS 的支持, 例如
<audio>
、<img>
、<link>
、<script>
和<video>
均有一个跨域属性 (crossOrigin property),它允许你配置元素获取数据的 CORS 请求。...
默认情况下(即未指定 crossOrigin 属性时),CORS 根本不会使用。如 Terminology section of the CORS specification 中的描述,在非同源情况下,设置 "anonymous" 关键字将不会通过 cookies,客户端 SSL 证书或 HTTP 认证交换用户凭据。
也就是说,请求没有携带origin
头的原因是没有给标签配置crossorigin
属性,由于我需要跨域加载的都是静态资源,不需要携带凭据,所以只要给各种标签都配置crossorigin="anonymous"
就可以了
于是我开始疯狂的搜索项目中的各种<img>
、<link>
、<script>
标签然后加跨域属性,发布,测试,很好,请求都带上origin
头了,并且成功成为了Response-Type: cors
的“透明”响应被 Workbox 缓存,储存空间的占用也恢复了正常水平
别高兴的太早
对于多媒体标签的跨域问题是解决了,其它情况下的请求又如何呢
经过测试与排查,我发现以下几种情况依然会得到不透明响应
- 在 CSS 中引用的背景图片(
background-image
)等 - 通过 JS 控制的但没有设置跨域属性的多媒体加载:常见于各类第三方组件,拿图片懒加载组件举例,一般的懒加载都是先通过 JS 请求图片,加载完成后再给
<img>
的src
赋值,如果这次由 JS 发出的请求没有携带origin
头,那么即使你给懒加载的<img>
设置跨域属性,你依然会得到不透明响应
解决办法:
- 改用
<img>
做背景图,你可以通过position: absolute
之类的 CSS 手段让<img>
不占据空间,达到和background-image
一样的效果 - 这个解决方法因实际情况而异了,例如我使用的是 Vue-Lazyload 组件,好在它提供懒加载组件的功能,因此我将原本的懒加载图片改为懒加载一个包含
<img crossorigin="anonymous">
的组件,这样就不存在用 JS 预加载图片的过程,曲线救国
另外,为了确保不会缓存到不透明响应,强烈建议配置 Workbox 时不要允许缓存返回码为0
的请求
例如我使用 Vue 的 PWA 插件,原本的配置(局部)是这样的:
module.exports = {
pwa: {
workboxOptions: {
runtimeCaching: [
{
urlPattern: /.../,
handler: "CacheFirst",
options: {
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
}
}
};
因为最初写的时候照抄了官方示例因此带上了0
,发生这次事件以后,我将statuses: [0, 200]
改为了statuses: [200]
:)
清除旧缓存
做完了以上步骤仍没有结束,因为用户如果在之前使用过旧版本的 PWA 应用,遗留的不透明请求缓存可能会产生副作用
如果你的缓存机制为CacheFirst
,仍会得到由 Service Worker 返回的不透明请求,由于crossorigin="anonymous"
的应用,这些请求将会被浏览器拒绝,导致资源无法加载
我个人的解决方法是在更新后判断是否是从之前的旧版本升级的,如果是的话调用 CacheStorage 的 API 清除运行时缓存并重载页面
例如在页面比较前的位置加入下方的 script
(function() {
var versionFlag = localStorage.getItem('versionFlag');
var currentFlag = '20191211'; // 随便使用一个字符串
// caches 是 CacheStorage 的一个实例
if (!(versionFlag && versionFlag === currentFlag) && caches) {
caches.keys().then(function(names) {
Promise.all(
names
.filter(function(name) {
return /runtime/.test(name);
})
.map(function(name) {
return caches.delete(name);
})
).then(function() {
localStorage.setItem('versionFlag', currentFlag);
window.location.reload();
});
});
}
})();