前言
HTTP 缓存作为前端工程师必备是知识点之一,相信在座的各位都已经掌握了。本文将对 HTTP 缓存知识进行查缺补漏,首先抛出几个问题:
- 当你想对某个文件进行缓存时,缓存是由客户端还是服务端来设置,或者都可以?
- 客户端和服务端都设置了 Cache-Control 时,这时缓存会以哪个为准?
- Cache-Control 的两个值 max-age=0 和 no-cache 效果是一样的吗?
当你看到 Cache-Control 是通用首部字段(请求和响应报文都会使用的首部)的时候,你是不是会有些困惑请求头设置 Cache-Control 的作用,那就对了,本文将带你对 HTTP 缓存知识进行差缺补漏。
实践出真知,相信很多人对 HTTP 缓存也仅是书面上掌握,并未实践过,故本文将使用 koa 搭建服务进行验证。
HTTP 缓存知识回顾
使用 HTTP 缓存可以减少带宽,缓解服务器压力,提高网站性能。与之有关的三个首部字段:Cache-Control, Expires, Pragma。
Pragma
Pragma 是 HTTP/1.1 之前版本的历史遗留字段, 仅作为与 HTTP/1.0的向后兼容而定义。Pragma 只有一个属性值 no-cache,只用在客户端发送的请求中,效果和 Cache-Control 的 no-cache 相同。
Pragma 的优先级高于 Cache-Control,我们看下面两个图:
图 1:仅设置 Cache-Control: max-age=15
图 2:设置 Cache-Control: max-age=15 和 Pragma: no-cache
从图 1 和图 2 的结果来看,两个属性都存在时是不使用缓存的,故Pragma 的优先级是高于 Cache-Control 的。
Expires
首部字段 Expires 会将资源失效的日期告知客户端,它的值是一个 HTTP 日期,例如Expires: Wed, 04 Jul 2012 08:26:05 GMT
,请求时间不超过这个值的时候,浏览器直接使用缓存。其优先级低于 Cache-Control,我们通过下面两个图来验证。
图 3:仅设置 Expires
图 4:同时设置 Expires 和 Cache-Control
从图 3 和图 4 可以看到,在同时设置 Expires 和 Cache-Control 的情况下,请求不走缓存,故 Expires 优先级低于 Cache-Control。
三个字段的优先级在此就出来: Pragma > Cache-Control > Expires
Cache-Control
Cache-Control 是 HTTP1.1 新增的通用首部字段,我们来看下它的指令:
图 5:请求指令(图片来源《图解 HTTP》)
图 6:响应指令(图片来源《图解 HTTP》)
请求首部中需要重点关注的有: no-cache, max-age。通常我们会使用 no-cache 或 max-age=0 来禁用缓存。
响应首部中需要重点关注的有: public, private, no-cache, no-store, max-age, s-maxage。no-store 表示禁用缓存,max-age 表示强缓存,no-cache 表示协商缓存,其他的看图中说明。
强缓存和协商缓存
当响应首部中出现 max-age 并且值大于 0 时,浏览器就会启用强缓存,意味着从现在开始,在 max-age 秒内,浏览器不会向服务端发起请求,会直接使用本地缓存。只有在超过 max-age 秒后,浏览器才会重新向服务端发起请求。
图 7:强缓存
我们看图 7:
- 浏览器第一次发起请求。
- 服务端返回 200,并且设置 Cache-Control: max-age=3600,意味着在接下来的 3600 秒内,浏览器可以直接使用本地缓存,不用向服务端发起请求。
- 浏览器再一次发起请求。
- 直接读取缓存。
图 8:协商缓存
接着从图 8 来看协商缓存:
- 浏览器第一次发起请求
- 服务端返回 200,设置 Cache-Control: no-cache 和 etag 值
- 浏览器再一次发起请求会带上之前返回的 etag 值,设置 If-None-Match=etag
- 此时服务端判断最新的 etag 值是否和请求头的 etag 值相同,相同就返回 304,不同就返回 200 和最新的内容。
- 接收到 304 状态码,从缓存读取内容
- 缓存返回内容
协商缓存除了 Etag 和 If-None-Match 这对组合外,还有 Last-Modified 和 If-Modified-Since,一个是根据文件的内容是否有修改,一个是根据文件的更新时间。
缓存整体流程
有了以上的知识作为基础,我们就可以知道 HTTP 缓存的整体运作流程,如下图:
图 9:HTTP 缓存整体流程
HTTP 缓存实践
缓存的设置
我们将单独在请求首部和响应首部设置缓存,看下什么情况下缓存会生效。
图 10:响应首部设置缓存,第二次发起请求
图 11:请求首部设置缓存,第二次发起请求
从图 10 和图 11 来看,我们就知道了,缓存只有在服务端设置才会生效。
请求首部和响应首部都设置了 Cache-Control
第一种情况,都设置了 max-age。
图 12:请求首部设置 max-age=10,响应首部设置 max-age=60
图 13:请求首部设置 max-age=0,响应首部设置 max-age=60
从图 12 中可以看到时间已经过去 50 多秒,浏览器依旧读取缓存,故是同时设置 max-age 情况下是响应首部在起效。
有一种例外情况,从图 13 可以看到,请求首部设置 max-age=0,此时是走协商缓存而不是强缓存。
第二种情况,响应首部设置 max-age,请求首部设置 no-cache
图 14:响应首部设置 max-age,请求首部设置 no-cache
多次发起情况的结果都同图 14,可以看到此时缓存不会启用,同时注意多次发起请求,都不会走协商缓存,直接都是不使用缓存。
有兴趣的同学可以看下 Chrome DevTool 的 Disable Cache 功能就是在请求首部就 no-cache
第三种情况,请求首部设置 max-age,响应首部设置 no-cache
图 15:请求首部设置 max-age,响应首部设置 no-cache
很明显,这个情况是走协商缓存,即请求首部的 max-age 只有在等于 0 的情况下才会有效果。
Cache-Control 的两个值 max-age=0 和 no-cache 效果是一样的吗
这个分请求首部和响应首部来看。从上面的例子,我们可以看到请求首部下,这两个效果是不一样的。no-cache 是禁用缓存,max-age=0 会走协商缓存。接下来我们看下响应首部情况。
图 16:响应首部设置 no-cache
图 17:响应首部设置 max-age=0
从上面两个图可以看出,max-age=0 和 no-cache 在响应头部效果是一样的。
这边顺便提一下,请求头部设置 no-cache 是禁用缓存,那请求头部的 no-store 是什么用,看 MDN 的介绍
图 18:请求头部的 no-store
可以看到大部分浏览器不会支持这个属性。
总结
对于开头的三个问题,做个总结:
- 服务端设置缓存才会有效果。
- 缓存通常是以服务端为准,但是客户端设置 Cache-Control 为 no-cache 或者 max-age=0 会改变缓存机制的决策。
- 客户端设置时效果不同,no-cache 是禁用缓存,max-age=0 是协商缓存。服务端设置时效果一致,都为协商缓存。
通过这次的查缺补漏,相信你对 HTTP 缓存有了更深的了解。
最后实践才是真道理,这次实践的源码,有兴趣的同学可以自己去尝试一番。
实践基于 Koa 和 Chrome 浏览器,如有错误,欢迎指正。