HTTP 缓存时间一直让开发者头疼。时间太短,性能不够好;时间太长,更新不及时。当遇到严重问题需紧急修复时,尽管后端文件可快速替换,但前端文件仍从本地缓存加载,导致更新长时间无法生效。
对于这个问题,很多网站都有相应的解决方案。
最常见的方案,就是为静态资源设置很长的缓存时间,然后在文件名中加入版本号或 Hash 值等字符,这样文件更新后 URL 会变化,即可避开之前版本的缓存。
不过这种方案也有缺陷。假设网页中有如下依赖关系的文件:
HTML
└─JS
└─CSS
└─GIF
现在需紧急更新 GIF 文件,于是重新发布 GIF 得到新的 URL 。然而 CSS 中引用的仍是之前的 GIF URL,因此 CSS 也要修改和发布,从而得到新的 CSS URL,进而导致加载该 CSS 的 JS 也得更新,加载该 JS 的 HTML 也得更新。
由此可见,快速更新一个文件需更新整个依赖链,白白浪费不少流量。同时产生大量无意义的新版本文件,它们的业务功能并无变化,只是为了修改其中的 URL 而已。
如果能通过 JS 删除某个文件的 HTTP 缓存,这样就不用更换 URL 了,更不必修改其他文件。是否有这样的黑科技?
现代浏览器添加了很多新特性,其中有些和缓存相关。
例如 Clear-Site-Data 特性。当 HTTP 存在 Clear-Site-Data: "cache"
响应头时,即可删除该站点的缓存文件。不过它会删除所有文件,而无法指定文件,因此不适合本案例。
例如 Fetch API 中 Request 对象的 cache 属性也涉及缓存操作。阅读文档,其中 no-cache
非常有趣:
no-cache
— The browser looks for a matching request in its HTTP cache.
- If there is a match, fresh or stale, the browser will make a conditional request to the remote server. If the server indicates that the resource has not changed, it will be returned from the cache. Otherwise the resource will be downloaded from the server and the cache will be updated.
- If there is no match, the browser will make a normal request, and will update the cache with the downloaded resource.
我们先看第一段:如果请求的文件存在缓存,无论是否过期,浏览器都会和后端协商缓存。未变化则使用本地缓存( HTTP 304 ),有变化则重新下载并 更新缓存。
虽然无法删除 HTTP 缓存,但能更新缓存内容,也是不错的。
基于该特性,这里做了个演示: https://www.etherdream.com/cache-purge/
该页面引用了 res.js ,其内容每隔 5s 递增,可通过页面中 Res Ver
显示。
由于 res.js
设置了很长的缓存时间,因此不断刷新页面 Res Ver
也不会变化。( Chrome 刷新时只有页面走协商缓存,内部资源仍从本地缓存加载)
点击 Purge
按钮,再次刷新页面,这时 Res Ver
变化了。之后不断刷新,数字仍保持不变。
通过控制台可见,no-cache
请求不会直接使用本地缓存,并且返回的内容会覆盖本地缓存。从而实现 HTTP 长缓存资源也能热更新!
如果 5s 内再次点击 purge,文件不会重新下载,而是协商返回 304 状态,符合文档中的描述。
这个特性如何应用到实际业务中?首先看下兼容性:
目前绝大多数浏览器都支持。如果不考虑低版本浏览器,那该如何使用?
显然需要一个清单列表,记录哪些文件需热更新。同时为了防止重复更新,还需记录每个文件的版本号:
/foo.gif 100
/bar.gif 101
...
热更新后将 URL 和版本号记录到本地存储中。之后再次执行时,如果版本号和本地存储中的相同,就不必再更新了。
至于什么时候执行,最简单的当然是页面打开时执行,但这不是最好的,因为:
每次打开页面都要加载清单文件,增加请求
热更新需要一定的时间(包括加载清单),页面初始化时不会等你,它仍使用现有的缓存资源
页面运行时定期执行,可解决第 2 个问题,从而在下次访问页面之前完成更新,但轮询清单会带来更多请求。
如需减少请求,可使用服务端推送技术,当清单变化时主动推送给前端。这样不仅可减少请求,而且前端能在第一时间更新。
不过这个方案仍有缺陷。我们的初衷是删除缓存,等业务再次使用时才下载;而本方案却是更新缓存,提前将文件下载到本地,附带预加载的功能。
如果热更新的是常用的公共文件倒还好,但如果只是小部分用户才会用到的文件,却让所有用户都提前预加载,这显然不合理,反而更浪费流量。
即使不能操作 HTTP 缓存,但如果能拦截 HTTP 请求并返回自定义内容,同样可达到热更新的效果。这正是 Service Worker API 的设计初衷。
开发者只需提供一个清单,将原始 URL 对应到最新 URL:
/foo.gif
https://cdn.mysite.com/1.0.0/foo.gif
/bar.gif
https://cdn.mysite.com/1.0.1/bar.gif
Service Worker 根据清单中的地址反向代理。这样,只需更新一个清单文件,即可更新所有 URL 的内容。
得益于 Service Worker 能在浏览器后台持续运行,因此其创建的 WebSocket 可长时间保持连接状态,结合后端的更新推送服务,可大幅减少加载清单的开销。
基于这个功能实现的演示: https://github.com/EtherDream/freecdn/tree/master/examples/quick-update
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.