查看原文
其他

【第1595期】可能是最被误用的 HTTP 响应头之一 Cache-Control: must-revalidate

紫云飞 前端早读课 2019-08-06

前言

今日早读文章由阿里妈妈@紫云飞授权分享。

本文首发于阿里妈妈前端快爆

正文从这开始~~

在 HTTP 客户端(浏览器或者缓存服务器)上,如果某个 URL 对应的缓存过期了,客户端会再次向该 URL 发送一个条件请求(带有If-Modified-Since/If-None-Match请求头),如果服务端(缓存服务器或者源站)返回的状态码是 304(没有响应体),则客户端会根据该304响应所包含的一些响应头(Date、Last-Modified、Cache-Control等)重新计算出这条缓存的过期时间,比如:

HTTP/2 304
Cache-Control: max-age=86400

这样的 304响应,就能让这条缓存重新续命一天;如果返回的状态码是 200,则整条缓存会被新返回的响应体替换掉。无论是哪种情况,这条缓存都重新变的有效了,HTTP 规范里把这一“让过期的缓存重新变的有效”过程,叫做 revalidate,英语翻译过来应该是“使重新生效”。

不过使动用法总是让人不好记忆,你也可以把 revalidate 理解成“再次校验”的意思:再次校验看看缓存是不是真的过期了,真过期了的话返回 200,假过期(客户端判断为过期了,但服务端说并没有)的话返回 304。

所以现在知道了,revalidate 是个常见的动作,缓存过期就会 revalidate ,缓存过期就会 revalidate ,缓存过期就会 revalidate ,说三遍,revalidate 不需要专门的指令。

搞清楚了 revalidate 这个词的含义,那么再来推测一下 must-revalidate是用来做什么的。must-revalidate直译过来是“必须再次校验”,那是不是说每次使用缓存前都要先校验一遍?即便没有过期?于是很多人按照这样的推测,写出了如下的 Cache-Control头:

Cache-Control: max-age=86400, must-revalidate

写这个配置的人想表示的是:该缓存有效期为一天,在这一天内,每次使用缓存前要先校验一遍才能使用。可试试就知道了,这里的must-revalidate 并不会生效,这条缓存仍然是直接读取了本地。

这是为什么呢?是因为must-revalidate生效有个前提,前提就是这个缓存必须已经过期,也就是说,必须一天以后,这个must-revalidate才可能发挥作用,规范里说的原话是:

The “must-revalidate” response directive indicates that once it has become stale, a cache MUST NOT use the response to satisfy subsequent requests without successful validation on the origin server.

翻译过来大概就是,must-revalidate指令是用来表示在一个缓存过期之后,不能直接使用这个过期的缓存,必须校验之后才能使用。哎?What?回忆一下刚才重复三遍的话:“缓存过期就会 revalidate”,revalidate 是缓存过期后自然而然的表现,怎么还需要专门的指令呢?

再细读规范就知道了,原来 must-revalidate生效的场景还有一个大前提,那就是 HTTP 规范是允许客户端在某些特殊情况下直接使用过期缓存的,比如校验请求发送失败的时候,还比如有配置一些特殊指令(stale-while-revalidate、stale-if-error等)的时候,原文是这样的:

A cache MUST NOT send stale responses unless it is disconnected (i.e., it cannot contact the origin server or otherwise find a forward path) or doing so is explicitly allowed

而must-revalidate的作用就是让那个“unless”失效 ,带有 must-revalidate 的缓存,在任何情况下,都必须成功 revalidate 后才能使用,没有例外。

各种缓存服务器软件,比如 NGINX、Vanish、Squid 都或多或少的允许通过Cache-Control指令或者修改软件配置的方式返回过期缓存,同时它们也都遵循了 HTTP 规范,加上must-revalidate的确能阻止返回过期缓存的行为。国内各大 CDN 厂商应该用的都是自研软件,不确定支持不支持返回过期缓存,所以 must-revalidate在国内网络环境能不能派上用场也不太确定。

更加不确定的是,很多用了 must-revalidate 指令的人真的知道它的作用是什么吗?现在回到我前面举的那个希望每次使用缓存前先校验一遍的例子:

Cache-Control: max-age=86400, must-revalidate

写出这个配置的人其实真正想要的是Cache-Control: no-cache。Cache-Control的几个指令特别容易混淆,不能望文生义。比如no-cache,并不是指不能用 cache,客户端仍会把带有 no-cache 的响应缓存下来,只不过每次不会直接用缓存,而得先 revalidate 一下,所以其实no-cache真正合适的名字才是 must-revalidate。而现在的must-revalidate更合适的名字可能是 never-return-stale。如果你想让客户端完全不缓存响应,应该用no-store,带有no-store的响应不会被缓存到任意的磁盘或者内存里,它才是真正的 no-cache。

计算机领域有个名言警句:

There are only two hard problems in Computer Science: cache invalidation, and naming things.(计算机领域只有有两大难题,“让缓存失效”和“给东西命名”)

而给上面讲的这些 Cache-Control指令命名,就是在给 HTTP 缓存失效相关的东西命名,恰好是两个难题撞到一起了,难上加难,才造成了这乱糟糟的局面。

读到这里你可能已经晕了,但本文才讲了一半。我上面只讲了must-revalidate在缓存服务器上的作用,还没说在浏览器上的作用。既然我说了must-revalidate更合适的名字是 never-return-stale,那浏览器有没有 return stale 的情况呢,也就是说浏览器会不会使用过期缓存呢?

还真有,那就是浏览器的后退前进功能。当点击 back/forwrad 按钮时,浏览器会尽量用本地缓存来重新打开页面,即便缓存已经过期了,也不会 revalidate。那must-revalidate能阻止这一行为,强迫该缓存 revalidate 吗?答案是并不能,甚至no-cache也不行,只有比no-cache更强劲的no-store才可以,因为硬盘上都没有缓存,浏览器想用也没法用啊。

另外值得注意的是,如果真有上面这个需求,未来可能no-store也帮不了你,因为 Chrome 目前在实现 bfcache,如果实现了,在页面前进后退时,页面内容会直接从内存缓存里读取,页面甚至都不会重新加载,连 JS 变量都保存着上次的值。

上面三段总结一下就是,must-revalidate的本职工作在浏览器端并没有发挥作用。那是不是说浏览器们就完全就不认什么 must-revalidate,连解析都没解析它?在 Firefox 和 Safari 里可能还真是这样的,但在 Chrome 里,不知道什么人手贱给 must-revalidate实现了一个规范里并没有要求的功能,下面我来说说到底是什么功能。

想让一个资源能缓存,有三种方式,按照解析优先级排序如下:

  • HTTP 1.1 风格的Cache-Control 响应头中的 max-age指令

  • HTTP 1.0 风格的 Expires 响应头

  • Last-Modified响应头

很多人只知道前两个,还知道 1 比 2 优先级高,但不知道第 3 个,我举个例子:

HTTP/2 200
Date: Wed, 27 Mar 2019 22:00:00 GMT
Last-Modified: Wed, 27 Mar 2019 12:00:00 GMT

上面这个响应,没有Cache-Control,也没有 Expires,但它其实也可以被缓存,可缓存时长是用 Date响应头的时间减去Last-Modified的时间,得出的时长再除以10,用汉语描述的话,就是用这个文件最近一次更新到现在的十分之一时长作为可缓存时长,这个例子的话,计算出的可缓存时长是一小时。

这种可缓存时长的算法用代码表示是 (date_value - last_modified_value) 0.10, 它是由 HTTP 规范推荐的算法,但规范中仅仅是推荐而已,并没有做强制要求,比如 Firefox 中就在这个算法的基础上还和 7 天时长取了一次最小值,是 min(one-week, (date_value - last_modified_value) 0.10) 。

扯远了,我们把这第 3 种缓存方式叫做启发式的(heuristic)缓存,你可以理解是没有显示的用前两种方式设置缓存时长的话,浏览器就会用这种隐式的缓存方式。

如果你想禁用由 Last-Modified响应头造成的启发式缓存,正确的做法是要加上 Cache-Control: no-cache,但在 Chrome 中,Cache-Control: must-revalidate也有同样的功效。很多人在 Chrome 里开发和测试,所以误以为这是 must-revalidate的正规作用,从而推断其它浏览器也是支持的,但其实规范里并没有指出 must-revalidate有关闭启发式缓存的功效,其它浏览器也都不支持这种用法。Chrome 里的这行代码加于 2008 年,看注释像是 Chrome 最早的开发工程师,现在 Chrome 的 VP Darin Fisher 写的。

好了,差不多讲完了, 再来句总结:must-revalidate在缓存服务器上有一点点作用,但比较小众;在浏览器端几乎没有任何作用。绝大多数情况,人们都是把它误用为 no-cache了。或者是完全没细研究,直接把max-age=0, no-cache, no-store, must-revalidate一坨都塞进去了,反正能 work 就不管了。

关于本文
作者:@紫云飞
原文:https://zhuanlan.zhihu.com/p/60357719

他曾分享过


【第1565期】浏览器中的画中画(Picture-in-Picture)模式及其 API


为你推荐


【第1574期】浏览器帧原理剖析


【第1541期】资源优先级 – 让浏览器助您一臂之力


【第1285期】我知道的HTTP请求

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存