1. 简介

浏览器缓存机制有四个方面:Memory Cache、Service Worker Cache、HTTP Cache、Push Cache。

对于前端开发工程师来说,比较熟悉的就是 HTTP 缓存,这也是每一个前端工程师都要掌握的知识点,下面我们一起来学习 HTTP 缓存,争取通过这篇文章就彻底掌握 HTTP 缓存。

HTTP 缓存机制是根据 HTTP 报文的缓存标识进行的。HTTP 缓存分为强缓存和协商缓存。优先级最高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。接下来我们一起来看下这两个缓存机制

2. 强缓存

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 Expires 和 Cache-Control 判断目标资源是否“命中”强缓存,如果命中则直接从缓存中获取资源,不会再与服务端发生通信。命中强缓存的情况下,返回的 HTTP 状态码为 200。

2.1 Expires

过去我们一直使用 Expires 来实现强缓存:当服务器返回响应时,在 Response Headers 中将过期时间写入 Expires 字段。例如:

img

从上图我们可以看到 Expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,就直接从缓存中获取这个资源。

到这里聪明的你可能已经发现了下面这个问题:由于 expires 的时间戳是服务器定义的,而本地时间的取值来自客户端,因此 expires 的工作机制对于客户端时间和服务器时间的一致性要求极高,如果两者的时间存在时差,会带来意料之外的结果。

由于上面的原因,加上 expires 是 HTTP1.0 的产物,现在实现强缓存大多数是使用 Cache-Control。

2.2 Cache-Control

Cache-Control 是 HTTP1.1 提出的特性,为了弥补 Expires 缺陷提出的,提供了更精确细致的缓存功能。

Cache-Control 包含的值很多:

  • public:表明响应可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。
  • private:表明响应只能被客户端缓存。
  • no-cache:跳过强缓存,直接进入协商缓存阶段。
  • no-store:表示当前请求资源禁用缓存
  • max-age=:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
  • s-maxage=:覆盖 max-age 或者 Expires 头。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。

这里需要注意的是:s-maxage 仅在代理服务器中生效,客户端只需要考虑 max-age。

下面我们来看个例子:

img

从上面的例子我们可以看到,HTTP 响应报文中同时有 Cache-Control 和 Expires 两个字段,由于 Cache-Control 优先级较高,那么直接根据 Cache-Control 的值进行缓存,也就是说在 315360000 秒内重新发起该请求,会直接使用缓存结果,强制缓存生效。

在 HTTP1.1 标准试图将缓存相关配置收敛进 Cache-Control 这样的大背景下, max-age 可以视作是对 expires 能力的补位/替换。在当下的前端实践里,我们普遍会倾向于使用 max-age。但如果你的应用对向下兼容有强诉求,那么 expires 仍然是不可缺少的。

3. 协商缓存

协商缓存,也称为对比缓存。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。

img

协商缓存依赖于服务端与浏览器之间的通信。

同样,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified 和 Etag,其中 Etag 的优先级比 Last-Modified 高。

3.1 Last-Modified & If-Modified-Since

Last-Modified(Response Header)和 If-Modified-Since(Request Header)是一对报文头,属于HTTP1.0。

Last-Modified 表示资源的最后修改时间,是一个时间戳,如果启用了协商缓存,它会在首次请求时随着 Response Headers 返回。

Last-Modified: Sat, 09 May 2020 09:33:56 GMT

If-Modified-Since 是一个请求首部字段,并且只能用在 GET 或 HEAD 请求中。客户端再次请求服务器时,请求头会包含这个字段,后面跟着在缓存中获取的资源的最后修改时间。

If-Modified-Since: Sat, 09 May 2020 09:33:56 GMT

服务端收到请求发现此请求头中有 If-Modified-Since 字段,会与被请求资源的最后修改时间进行对比,如果一致则会返回 304 和响应报文头,浏览器从缓存中获取数据即可。从字面上看,就是说从某个时间节点开始看,是否被修改了,如果被修改了,就返回整个数据和200 OK,如果没有被修改,服务端只要返回响应头报文,304 Not Modified,Response Headers 不会再添加 Last-Modified 字段。

使用 Last-Modified 是有一定缺陷的:

  • 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为 If-Modified-Since 只能检查到以秒为最小计量单位的时间差。
  • 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
  • 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。

为了解决上面服务器没有正确感知文件变化的问题,Etag 作为 Last-Modified 的补充出现了。

3.2 Etag & If-None-Match

Etag 和 If-None-Match 是一对报文头,属于 HTTP1.1。

Etag 是一个响应首部字段,是根据实体内容生成的一段 hash 字符串,标识资源的状态,由服务端产生。

ETag: W/"324023994867772d0dd9fac01f1420bd"

If-None-Match 是一个条件式的请求首部,如果请求资源时在请求首部加上这个字段,值为之前服务器返回的 Etag,则当且仅当服务器上没有任务资源的 Etag 属性值与这个值相符,服务器才会返回带有请求资源实体的 200 响应,否正服务器会返回不带实体的 304 响应。

If-None-Match: W/"324023994867772d0dd9fac01f1420bd"

Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的:Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。

4. 如何设置一个可靠的缓存规则

上面我们已经学完了 HTTP 缓存机制,是不是迫不及待想要实践一番?但是如何设置一个可靠的缓存规则,需要根据实际需求决定,绝大部分需求的缓存规则都可以根据 Chrome 官方提供的流程图来进行设置。

img

我们一起来看上面这张流程图:

  • 如果资源不可复用,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;
  • 如果资源可复用,考虑是否每次都需要向服务器进行缓存确认,如果是,设置 Cache-Control 的值为 no-cache;
  • 如果不需要每次都向服务器确认,考虑资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;
  • 接下来考虑资源的过期时间,设置对应的 max-age;
  • 最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。

后续根据这个流程,我们就可以设计出可靠的缓存规则了。