对于前端缓存,文章很多。然而相当一部分文章没有什么条理,罗列了很多Cache-Control,Expires,Etag等。然而在实际使用的时候,确认很多同学看的很混乱,最后也不知道该如何制定一个实践方案。本文采取结论先行的办法,直接说明缓存的最佳实践。之后在具体说明其中内容。帮助同学先知其然,然后知其所以然。
缓存实践结论
- 频繁变动的资源,使用协商缓存 Cache-Control: no-cache。
HTML 文件
html页面缓存的设置主要是在< head >标签中嵌入< meta >标签,这种方式只对页面有效,对页面上的资源无效
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="Cache-Control" content="max-age=7200">
2.不常变化的资源,静态资源,使用强缓存并配合文件名添加HASH。这也是为什么前端打包的文件需要加上hash值的原因。
Cache-Control: max-age=31536000 CSS,JS,图片: 给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年) 给文件名加上hash值:
webpack给我们提供了三种哈希值计算方式:
- hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。(不用这个)
- chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
- contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。
那么这里就出现了两个很重要的概念。强缓存和协商缓存
强缓存
强缓存不会发送请求,直接从浏览器加载资源。 是否命中强缓存根据HTTP Response头部Expires、Cache-Control(max-age)来判断。 注意 Cache-Control(max-age)是http1.1出现的。是更合理的方案。至于Expires的不合理处如下所示。看看就行。用Cache-Control旧可以
- Expires通过返回一个过期时间来判断是否过期,在此时间之前浏览器直接从缓存加载资源。但其缺点是返回的过期时间为服务器时间,而比较是同客户端时间比较,如果服务端和客户端存在时间误差就不准了。
- max-age返回的时间过期时间跨度,比如max-age=3600告诉浏览器接下来的1小时内使用缓存。这样就解决了Expires时间误差导致的问题。
服务端代码演示,koa2
app.use(async (ctx) => {
const url = ctx.request.url if (url === '/')
{ // 访问根路径返回index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html') }
else { const filePath = path.resolve(__dirname, `.${url}`) // 设置类型
ctx.set('Content-Type', parseMime(url)) // 设置 Cache-Control 响应头
ctx.set('Cache-Control', 'max-age=30') // 设置传输
ctx.body = await parseStatic(filePath) }
})
**
协商缓存
请求的资源通过资源标识与服务器协商比对,协商成功,服务器返回304状态码,浏览器从本地加载。协商缓存需要用到 Etag 字段 与 if-none-match,Etag 是 HTTP 响应头中的字段,Etag 的值是根据资源内容编码生成的一段字符串(资源标识),内容不同就会生成不同的Etag。你可能在网上看到还有 Last-modified ,了解即可。
再次发起请求时,请求头会带有 if-none-match 字段,值为上一次响应的 Etag(没有则不会携带)。服务器会将请求的资源生成资源标识与发送过来的值进行比对,比如果比对成功则返回 304 状态码,浏览器从本地加载该资源。
如果协商失败服务器会返回新的资源+新的Etag(资源标识)
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 访问根路径返回index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html') }
else { const filePath = path.resolve(__dirname, `.${url}`)
const fileBuffer = await parseStatic(filePath)
const ifNoneMatch = ctx.request.header['if-none-match']
// 生产内容hash值
const hash = crypto.createHash('md5')
hash.update(fileBuffer)
const etag = `"${hash.digest('hex')}"`
ctx.set('Cache-Control', 'no-cache')
ctx.set('Content-Type', parseMime(url))
// 对比hash值
if (ifNoneMatch === etag) {
ctx.status = 304 }
else {
ctx.set('etag', etag)
ctx.body = fileBuffer
}
} })
综合来说,在请求时候,先命中强缓存,失效后使用协商缓存。就概念来说cache-control,Etag,if-none-match是我们必须知道。如Expires,Last-modified为http1.0时代产物。了解就行。项目也不要使用。最后是缓存流程图。这里注意,本文只介绍前端的缓存。一般来说ng也是可以缓存。这里不做介绍
缓存分类
memory cache
Memory Cache 也就是内存中的缓存
优点:
读取速度快
缺点:
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
disk cache
Disk Cache 也就是存储在硬盘中的缓存
优点:
缓存再硬盘中,容量大
缺点:
读取速度满
对于大文件来说,大概率是不存储在内存中的,反之优先
当前系统内存使用率高的话,文件优先存储进硬盘
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。 传输协议必须为 HTTPS Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
不常用
Push Cache
- Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。
- 它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂
不常用
刷新操作对缓存的影响?
- 正常操作(输入地址URL、跳转连接、前进后退等等):强缓存与协商缓存
均有效。 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。 - 手动刷新(f5、点击菜单刷新、右键刷新):强缓存与协商缓存
均有效。 因为 tab 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache - 强制刷新(ctrl + f5):
强缓存、协商缓存均失效