神代綺凜

一个由不透明响应引发的灾难
不是应该只有 5MB 的缓存吗,怎么就“DOMException: Quota exceeded.”了?
扫描右侧二维码阅读全文
12
2019/12

一个由不透明响应引发的灾难

不是应该只有 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 the navigator.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 缓存,储存空间的占用也恢复了正常水平

别高兴的太早

对于多媒体标签的跨域问题是解决了,其它情况下的请求又如何呢

经过测试与排查,我发现以下几种情况依然会得到不透明响应

  1. 在 CSS 中引用的背景图片(background-image)等
  2. 通过 JS 控制的但没有设置跨域属性的多媒体加载:常见于各类第三方组件,拿图片懒加载组件举例,一般的懒加载都是先通过 JS 请求图片,加载完成后再给<img>src赋值,如果这次由 JS 发出的请求没有携带origin头,那么即使你给懒加载的<img>设置跨域属性,你依然会得到不透明响应

解决办法:

  1. 改用<img>做背景图,你可以通过position: absolute之类的 CSS 手段让<img>不占据空间,达到和background-image一样的效果
  2. 这个解决方法因实际情况而异了,例如我使用的是 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();
      });
    });
  }
})();
搬瓦工VPS优惠套餐,建站稳如狗,支持支付宝,循环出账94折优惠码BWH3HYATVBJW
年付$47CN2线路,1核/1G内存/20G硬盘/1T@1Gbps【点击购买】($28套餐已经不再销售)
年付$47CN2 GIA线路,1核/512MB内存/10G硬盘/500G@1Gbps【点击购买】(可能已售罄)
Last modification:February 12th, 2020 at 01:19 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment

9 comments

  1. 安忆  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.149(Google Chrome 80.0.3987.149)
    1. 神代綺凜  Mac OS X 10.15.3(Mac OS X 10.15.3) / Google Chrome 80.0.3987.149(Google Chrome 80.0.3987.149)
  2. 咸鱼干  Windows 7 x64 Edition(Windows 7 x64 Edition) / Google Chrome 60.0.3112.90(Google Chrome 60.0.3112.90)
    大佬moe.best这个Premium域名续费多少刀啊
    为啥我的.best Premium续费80刀
    1. 神代綺凜  Mac OS X(Mac OS X) / Safari(Safari)
      @咸鱼干 差不多也是这个价,70多刀
      1. 咸鱼干  Windows 7 x64 Edition(Windows 7 x64 Edition) / Google Chrome 60.0.3112.90(Google Chrome 60.0.3112.90)
        @神代綺凜 太套路了叭,注册不是Premium续费只要20来刀,快到期突然就涨价了
  3. Naiel  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 79.0.3945.88(Google Chrome 79.0.3945.88)
    一样遇到360背景图无法使用ssl问题。我是直接用cors-anywhere进行反代
    1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 79.0.3945.88(Google Chrome 79.0.3945.88)
      @Naiel 实际上是有支持 SSL 的 CDN 域名,就如我文章前面所写
      cors-anywhere 国内部分地区无法访问,大多数需要跨域调 API 的情况可以用 json2jsonp 代替
      1. Naiel  Android 9(Android 9) / Google Chrome 79.0.3945.93(Google Chrome 79.0.3945.93)
        @神代綺凜 也可以用miniProxy,只有一个php文件。
  4. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 79.0.3945.88(Google Chrome 79.0.3945.88)
    学到了学到了 ,关注麒麟果然能学到好多东西