缓存生效的情况下,浏览器会返回304状态码。协商缓存是在强制缓存失效之后,需要重新对比缓存,由服务器决定是否失效的一种机制。
304:请求的资源没有改变,但是被重定向到已缓冲的文件,所以也叫做缓存重定向,这个子弹主要是用于缓存处理。
协商缓存的请求步骤通常分为两步。
初次访问: 客户端请求依然需要发送到服务端,但是服务端会通知浏览器缓存请求响应信息,浏览器这时候会偷偷缓存请求。
第二至更多次访问: 1. 假设客户端请求资源已经过期,则在请求中携带Etag 访问服务器。 2. 服务器比对Etag进行校验,比较是否和本地一致。 3. 如果一致返回304,浏览器可以继续使用资源。 4. 如果不一致就需要重新返回请求结果,再次进行缓存。
协商缓存实际上就是浏览器和服务器进行协商通过协商的结果判断是否使用本地缓存。
主要涉及的请求头部
注意这里一共有两组四个字段进行判断,ETag
和If-None-Match
(一组), If-Modified-Since
字段与 Last-Modified
字段(一组),前者优先级比后者高,同时出现会出现“短路效应”。
ETag
和If-None-Match
两者的值都是资源的唯一标识符(实际为文件资源的哈希散列值)。
判断过程如下:
- 浏览器请求资源,服务器返回报文中加入
Etag
值,资源更新则Etag值也会更新。 - 浏览器再次请求资源,此时请求报文会加入
If-None-Match
,值为上一次响应报文的Etag值。 - 服务器比对报文的
If-None-Match
和当前的Etag
是否一致,不一致则更新Etag并且返回,下一次浏览器请求Etag将传输新的值。如果一致表示资源没有更新,状态码返回304,浏览器从本地缓存获取,此时响应头会同时返回Etag值。(虽然没有变化)
If-Modified-Since
字段与 Last-Modified
字段
位于请求头部的If-Modified-Since
字段以及位于响应头部中的 Last-Modified
字段
- 响应头部中的
Last-Modified
字段:表示资源的Last-Modified
( 最后修改时间)。 - 请求头部的
If-Modified-Since
字段:资源过期并且接收响应头部出现Last-Modified
(最后修改时间)声明,会发送此字段,并且此字段值等于Last-Modified
( 最后修改时间)。
判断流程如下:
- 浏览器第一次向服务端请求之后,服务端响应中加入
Last-Modified
字段,表示资源最后一次修改时间。 - 浏览器再次请求,在请求报文中会加入
If-Modified-Since
字段,字段值等于上一次浏览器返回的Last-Modified
(最后一次修改时间)。 - 服务器比对
Last-Modified
和If-Modified-Since
字段,如果不一致则接受资源并且返回更新之后的资源,如果一致表示资源没有更新,返回304状态码,此时浏览器会从本地缓存获取资源文件。值得注意的是,本地请求如果是304,此时响应头中不会再添加Last-Modified
字段。
协商缓存和强制缓存流程图
注意:If-None-Match 和 If-Modified-Since 的关系是:
If-None-Match && If-Modified-Since
。
下面的图和上面类似,不过把If-None-Match
和 If-Modified-Since
丰富了一下:
如果同时发送If-None-Match
、If-Modified-Since
字段怎么处理
此时服务器只要比较If-None-Match
和ETag
的内容是否一致即可,内容一致则返回304并且依然使用,不一致则返回新的请求结果,并且重新缓存。
If-None-Match
判断优先级总是要高一些,IETF同样如此建议。
Etag 和 Last-Modifed 对比
Etag
和Last-Modifed
效果是相同的,为什么相同的功能需要两个字段控制?
首先说一下结论,Etag
实际上更为推荐并且更常使用,因为它的细粒度更小,下面我们举个例子,再进行总结。
我们先来举个栗子,假设一个资源从资源请求开始到当前请求过去了120秒接近过期节点,并且浏览器启用了同一个资源的新请求,此时HTTP刚好卡在过期节点之前找到缓存。虽然查到的缓存在获取那一刻是没有过期的,但是他不能这么干,因为此时再浏览器看来响应已经过期了,浏览器需要重新发一个新请求获得完整响应。
上面的例子本意是好的,但是如果资源没有出现改变,就没有必要下载已经在缓存里面的信息,这么做明显是浪费请求资源。如果很多个请求都在这个临界点访问,这样会会造成服务器资源的浪费。
所以基于时间的判断是不可靠的,Etag标头负责检查文件内容的哈希码,浏览器不需要知道哈希算法,只需要拿客户端请求的Etag值和当前本地缓存比对即可,如果Etag值一致,哪怕此时请求资源响应已经过期了,依然可以用本地缓存文件返回,跳过下载步骤。
有了上面的案例,下面我们知道了为什么要引入Etag,下面是相关结论:
结论
- 有时候文件仅仅是改了日期(比如重新传了一份一模一样的覆盖),我们可以认为文件内容是没有改变的,依然可以用本地缓存而不是GET请求。
- 文件如果改动非常频繁但是内容没有改变,
Last-Modifed
是S(秒)级,很容易出现请求响应过期,并且频繁的重新下载。 - 某些服务器不能精确表示最后修改时间,只能给出笼统的日期。
总的来说就是基于时间的判断是不可靠的,使用Etag值可以更加精确控制缓存,所以引入服务器自动生成Etag校验码作为唯一标识符,如果资源频繁改动会重新生成Etag值,但是如果资源只是频繁的刷新日期,则Etag不变。
在默认的情况下这两个值可以一起使用,但是优先校验ETag(原因请看上面的栗子)。
用户行为对缓存影响
这里直接用了网上的一张图,主要记住几个无效的项即可。
下面是日常用户操作
- 大部分网站打开网页通常优先检查disk cache,有就使用,没有就发送请求。
- F5刷新,因为此时TAB没有关闭,此时使用memory cache 是可用的,会被优先使用,而disk cache 会被后面使用。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-control: no-cache
(为了兼容,还带了Pragma: no-cache
),此时返回200状态码和最新内容
缓存策略建议
因为缓存是作用于文件的,而文件资源基本情况有两种:频繁改动的资源和长久不变化的资源。
频繁变动的资源:
- 首先需要使用
Cache-Control: no-cache
使浏览器每次都请求服务。 - 配合
ETag
或者Last-Modified
来验证资源是否有效
这样的处理方法可以显著的减少响应内容的大小。
长久不变的资源: 直接给一个Max-age=31536000
一年的时间让浏览器强制使用缓存,所以通常建议在文件名中加入哈希码和版本号等信息,防止长久缓存文件突然需要更新的时候能及时反馈给客户端。