这个不是 http 协议规定的,而是在实际开发的过程中形成的一种共识
缓存的基本原理
在一个 C/S 架构中,最基本的缓存分为两种
- 客户端缓存
- 服务器缓存
这里单看客户端缓存
所谓客户端缓存,顾名思义,是将某一次的响应结果保存在客户端(比如浏览器)中,而后续的请求仅需要从缓存中读取即可,极大的降低了服务器的处理压力
客户端缓存的原理如下:
![image-20240416100016893](https://img-blog.csdnimg.cn/img_convert/df3e34012880b55fe638fc349ef1c4a3.png)
这只是一个简易的原理图,实际情况可能会有差异
这里涉及到一个缓存策略问题,这些问题包括:
- 缓存资源需要加入到那些缓存,那些不需要
- 缓存的时间是多久
- 如果服务器的资源有所改动,客户端如何更新缓存呢?
- 如果缓存过期了,可视服务器上的资源并没有发生变动,又改如何处理呢
- …
要回答这些问题,就必须要清楚http
中关于缓存的协议
理解了 http 的缓存协议,自然就可以回答这些问题了
来自服务器的缓存指令
当客户端发出一个 get 请求到服务器,服务器可能有以下的内心活动:『你请求的这个资源,我很少会改动它,干脆你把它缓存起来吧,以后就不要来烦我了』
为了表达美好愿望,服务器在响应头中加入了以下内容:
Cache-Control: max-age-3600
ETag: W/"121-171CA289ebf"
Date: Tue, Apr 16 2024 10:20:56 GMT
Last-Modified: Tue, Apr 16 2024 08:10:21 GMT
这些响应头表达的信息如下:
Cache-Control: max-age-3600
:我希望你把这个资源缓存起来,缓存时间是3600秒ETag: W/"121-171CA289ebf"
:这个资源的编号是W/"121-171CA289ebf"
Date: Tue, Apr 16 2024 10:20:56 GMT
:我给你响应这个资源的服务器时间是格林威治时间 2024-04-16 10:20:56Last-Modified: Tue, Apr 16 2024 08:10:21 GMT
:这个资源上一次修改的时间是格林威治时间 2024-04-16 08:10:21
这个美好的缓存愿望,就这样通过响应头传递给了客户端
如果客户端是其他应用程度,可能并不会理会服务器的愿望,也就是说,可能根本不会缓存人任何东西
但是凑巧客户端是浏览器,它和服务器一直以来都是相亲相爱的小伙伴,当他看到服务器的这些个响应头后,就会执行下面的操作:
- 浏览器吧这次请求得到的响应体缓存到本地文件中
- 浏览器标记这次请求的请求方法和请求路径
- 浏览器标记这次缓存的时间是3600秒
- 浏览器记录服务器的响应的时间,以这个时间为基准开始计时
- 浏览器记录服务器给予的资源编号 W/“121-171CA289ebf”
- 浏览器记录资源的上一次修改的时间
这一次的记录非常重要,它为以后浏览器要不要请求服务器提供了各种依据
来自客户端的缓存指令
当客户端收拾好行李,准备再次请求时,突然想起了一件事情,我需要的东西缓存里面有没有呢?
此时,客户端会到缓存中找寻是否有缓存的资源
寻找的过程如下:
- 缓存中是否有匹配的请求方法和路径?
- 如果有,该缓存资源是否还有效呢?
以上两个验证会导致浏览器产生不同的行为
![image-20240506030739521](https://img-blog.csdnimg.cn/img_convert/38743ebb573e9f3f4f5951880b1d732a.png)
为什么缓存失效还要带着缓存去请求呢?因为想看看这个资源是否还能使用,如果还可以就继续使用,所以从狭义上来说,这一步可以说是协商请求,而从广义的角度来看,整个流程都是协商请求
![image-20240506030628578](https://img-blog.csdnimg.cn/img_convert/75395f405e8317881f49ceb7c7abb2c6.png)
要验证是否有匹配的缓存非常简单,只需要验证当前的请求方法和当前的请求路径是否有对应的缓存存在即可
如果没有,就直接请求服务器,就和第一次请求服务器一样
关键在于验证缓存是否有效,如何验证?
使用 max-age + Date 得到一个过期时间,看看这个过期时间是否大于当前时间,如果大于,则表示缓存还没有过期,仍然有效,反之则无效
缓存有效
当浏览器发现缓存有效时,完全不会请求服务器,直接使用缓存即可得到结果
此时,如果你断开网络,会发现资源仍然可用
这种情况会极大的降低服务器压力,但当服务器更改了资源后,浏览器是不知道的,只要缓存有效,它就会直接使用缓存
缓存无效
当浏览器发现缓存已经过期,它并不会简单的把缓存删除,而是抱有一丝希望,想问问服务器,我这个缓存还能继续使用吗
于是,浏览器向服务器发送了一个带缓存的请求,又称之为协商缓存
所谓带缓存的请求,无非就是加入了以下的请求头:
If-Modified-Since: Tue, Apr 16 2024 08:10:21 GMT
If-None-Match: W/"121-171CA289ebf"
它们表达的信息如下:
If-Modified-Since:Tue, Apr 16 2024 08:10:21 GMT
,你好,你曾经告诉我这个资源上一次修改的时间是格林威治时间Tue Apr 16 2024 08:10:21 GMT,请问这个资源在这个时间后有发生改变吗If-None-Match: W/"121-171CA289ebf"
,你好,你曾经告诉我,这个资源的编号是 W/“121-171CA289ebf”,请问这个资源的编号发生了变动吗
其实,这两个问题就可以合并为一个问题,你的资源变动了吗?
之所以发两个信息,就是为了兼容不同的服务器,因为有些服务器只认二者之间的一个,而有些则都认
目前很多服务器只要发现
If-None-Match
,就不会去看If-Modified-Since
因为 If-Modified-Since 是 http1.0 的规范,而If-None-Match 是 http1.1的规范
此时,问题又抛给了服务器,接下来就轮到服务器 showtime 时刻了
服务器可能会产生两个情况:
- 缓存已经失效
- 缓存仍然有效
如果是缓存已经失效,那么非常简单,服务器在此给予一个正常的响应(响应码 200 带响应体),同时可以附带上新的缓存指令即可,这样一来,客户端缓存新的内容即可
如果是缓存仍然有效,它可以通过一种机器简单的方式告诉客户端:
- 响应码为
304 Not Modified
- 无响应体
- 响应头带上新的缓存指令,同来自服务器的缓存指令
这样一来,就相当于告诉客户端:『你的缓存资源仍然可用,我给你一个新的缓存时间,你那边更新一下就可以了』
于是,客户端就继续 happy 的使用缓存了
这样一来,可以最大程度的减少网络传输,因为资源还有效,服务器不会传输消息体
它们完整的交互过程如下:
![image-20240416174936811](https://img-blog.csdnimg.cn/img_convert/4d91f72efb18efa1d3a12158016b83fd.png)
细节补充
上述描述了客户端缓存的基本概念和过程,但其中仍然有不少细节值得注意:
Cache-Control
在上述的讲解中,Cache-Control
是服务器向客户端响应的一个消息头,它提供了一个 max-age
用于制定缓存时间
实际上,不仅可以设置 max-age,还可以设置下面一个或者多个值:
public
:表示服务器资源是公开的,比如有一个页面资源,所有人看到的都是一样的。这个值对于浏览器而言没有什么意义,但可能在某些场景可能有用。本着「我告知,你随意」的原则 http 协议中很多时候都是客户端或服务器告诉另一端详细的信息,至于另一端用不用,
完全看它自己private
:表示服务器资源是私有的,浏览器一样不做出处理,使用看自己no-cache
:告知客户端,你可以缓存这个资源,但是不要直接使用他,当你缓存之后,后续的每一次请求都需要附带缓存指令,让服务器告诉你这个资源有没有过期。这样的话,浏览器还是会缓存这个数据,但是不会直接缓存中读取,而是每一次都是协商缓存no-store
:告知客户端,不要对这个资源做任何的缓存,之后的每一次请求都按照正常的普通请求进行,若设置了这个值,则浏览器将不会对这个资源做任何的缓存处理
Expire
在 http1.0 版本中,是通过 Expire 响应头来指定过期时间点的,例如:
Expire: Tue, Apr 16 2024 08:10:21 GMT
到了 http1.1 版本,已更改为通过 Cache-Control 的 max-age 来记录了
记录缓存是的有效期
浏览器会按照服务器响应头的要求,自动记录缓存到本地文件,并设置各种相关信息
在这些信息中,有效期尤为关键,它决定了这个缓存可以使用多久
浏览器会根据服务器不同的响应情况,设置不同的有效期
具体的有效期设置,按照下面的流程进行:
![image-20240417014212636](https://img-blog.csdnimg.cn/img_convert/54bc644aa83dc48b9a0228c5da916b3d.png)
例如,当 max-age 设置 0 时,缓存立即过期
虽然立即过期,但缓存仍然被记录下来,后续的请求通过缓存指令发送到服务器,来确认资源是否被更改
因此,max-age 设置为 0,类似 no-cache
Pragma
这是 http1.0 版本的消息头
当该消息头出现在请求中时,是向服务器表达:不要考虑任何缓存,给我一个正常的结果
在 http1.1 版本中,可以在请求头中加入 Cache-Control:no-cache
实现同样的含义
是的,Cache-Control 可以出现在请求头中
在 chrome 浏览器中调试时,如果勾选了 Disable cache
,则发送的请求会附带该信息
Vary
有的时候,是否有缓存,不仅仅是判断请求和请求路径是否匹配,可能还要判断头部信息是否匹配
此时,就可以使用 vary 字段来指定要区分的消息头
比如,当使用 GET/personal.html
请求服务器时,请求头中的 cookie
的值不一样,得到的页面也不一样
如果还按照之前的做法,仅仅匹配请求方法和请求路径,如果 cookie
变动,你可能得到的仍然是之前的页面
这个字段也是服务器响应回来的时候可以设置的,比如:Vary: cookie
使用版本号或者hash
如果你是一个前端工程师,使用过vue
或者其他基于webpack
搭建的工程
你会发现打包的结果中很多文件名类似于这样:
app.68297cd8.css
文件的中间部分使用了 hash 值
这样做的好处就是可以让客户端大胆的、长时间的缓存该文件,减轻服务器的压力
当文件改动后,它的文件 hash 值也会随之改变,比如变成了 app.446ccb8.css
这样一来,客户段需要重新请求新的文件,就会发现路径已经变更了,由于之前的缓存路径无法匹配,就会重新获取资源了
以上是现代流行的做法
而在古老的年代,还没有构建工具出现时,人们使用的办法是在资源路径后面加入版本号来获取新版本的文件
比如,页面中引入一个 css 资源app.css
,它可能引入的方式是:
<link href="/app.css?v=1.0.0">
这样当文件发生改变,给予新的版本号就回重新请求