HTTP 协议的缓存机制涉及到多个请求头字段,而且整个缓存机制的细节行为也存在各种情况的差异,譬如说什么时候访问本地缓存不发送请求,什么时候发送请求查看资源是否更新,获取 response 什么情况下更新缓存等。以前我对此一知半解只是笼统的知道一些概念,譬如 Cache-Control 可以控制缓存的时间和是否需要缓存,但是缓存过期后的行为,有缓存后浏览器是否有 http 请求都不甚了解。所以特地 google 下,此篇是对此的知识梳理。
协议概述
什么情况下可以使用本地缓存?譬如说我们用 get 方式请求了一个资源 http://mytest.domain.com/static/images/bg.png
,那么我们下次再请求这个图片资源的时候符合哪些条件可以使用本地缓存呢?
- url 必须是
http://mytest.domain.com/static/images/bg.png
,如果是http://mytest.domain.com/static/images/bg.png?t=12312321
就会发起新的请求,因为 url 不同。 - 发送请求的 method 必须可被缓存,譬如 get。
- 第一次请求 response(即本地缓存)如果有一个 Vary 头,他的值列出的是一系列 http header,第二次请求的请求头中那些在 Vary 值中所列的头,必须和第一次请求相同(具体规则)。
- 第二次请求不包含请求头 Pragma: no-cache
- 第二次请求不包含请求头 Cache-Control: no-cache|max-age=0
- 第一次请求的 response(即本地缓存)不包含 Cache-Control: no-cache
- 本地缓存没有过期
- 或者虽然本地缓存已经过期,但是服务器验证缓存和服务器资源一致,允许使用本地缓存的情况(即获得304 response)
注1:上述任何提一个条件都可以被 cache-control extension 覆盖
注2:response header中不仅仅可以 Cache-Control: no-cache,还可以 Cache-Control: no-cache="Set-Cookie" 详见
如何计算本地缓存是否过期
浏览器是通过比较缓存剩余有效时间和当前缓存已存在时间来判断的:response_is_fresh = (freshness_lifetime > current_age)
。freshness_lifetime
取值优先级次序如下列表所示(排在上面的优先级越高):
- Cache-Control: s-maxage=xx
- Cache-Control: max-age=xx
- Expires: xxxxx
- 按规则进行计算(推测)
注1;如果有多个重复的上述头,那么是非法的,视作 response(资源)过期
freshness_lifetime 计算(推测)规则:
规范并没有给出具体的算法,但是给出了最坏情况(but does impose worst-case constraints on their results),如果 response(资源)有 Last-Modified 头,那么推荐用从当前到lastmodified这个时间段的 10% 作为 freshness_lifetime
,并且 response(资源)的 current_age 如果已经超过24小时,必须在这个 response 上加上113 warn-code头。很少有浏览器实现了freshness_lifetime
的自助计算(推测),所以还是还是鼓励给出上述显式的1 - 3三种情况。
current_age 计算规则:
泛泛来说就是 response(资源)在本地的驻足时间加上网络传输时间,因为网络传输时间的计算有多个条件,规范实在看的我头晕,所以具体计算规则详见规范
本地缓存过期,如何通过服务器验证缓存的是否依然有效(即304的情况)
首先,如果第一次的 response(资源)头中显示申明了一些禁止缓存的头(譬如:"no-store" or "no-cache" 等等),就不存在过期不过期的问题,因为这个资源不允许缓存。其次,过期缓存也不一定不可用,如果在断网或者第二次请求带上 max-stale 这个请求头,那么浏览器可以使用过期的缓存(masx-stale 表示在缓存过期后多少时间内浏览器依然可以使用缓存)。碰到过期缓存,浏览器可以发送一个条件请求(conditional request)。这个请求的 url 依然是第一次请求的 url,只是会带上些当前资源的一些信息,以供服务器验证这个缓存是否依然可用还是需要更新:
- response(资源)的 Last-Modified 头所带的值会放到条件请求的 If-Modified-Since 头中,或者是 If-Unmodified-Since 又或者 If-Range。
- response(资源)的 ETag 头所带的值会放到条件请求的 If-None-Match 头中,或者 If-Match 又或者 If-Range。
服务器会根据不同的条件请求头来验证资源,具体的行为详见规范,这里不细致展开。针对条件请求,服务器返回会有三种情况:
- 一个带有304 status code 的返回。表示缓存可以被更新和重用。
- 一个带有 body 的完整的 response。表示用这个 response 作为请求的返回,并且视条件可以用这个完整的 response 替换浏览器原有的缓存(此资源)。
- 如果返回一个5xx的 response,那么浏览器可以选择就显示这个5xx的返回,或者使用本地缓存(尽管可能是过期的)- 规范没有规定应该选择哪种处理,应该是取决于浏览器的行为。
如果是一个304的返回,规范说这个返回可以更新本地缓存,更新策略分三种:
- 如果这个304 response 带有资源有效性的强验证头,那么浏览器会寻找本地缓存,寻找那些带有同样强验证头的缓存,然后用这个最新的 response 去更新这些匹配的缓存(同一个资源可能浏览器保存有多份缓存,譬如日期不同等)。
- 如果这个304 response 带有资源有效性的弱验证头,那么浏览器同样会找相匹配的缓存,但是只会更新最新的那条匹配的缓存。
- 如果这个304 response 没有带有任何资源有效性验证头,并且浏览器缓存只有一份,并且这份也同样没带有任何资源有效性验证头,那么浏览器就会用这个304 response,更新本地缓存。
协议流程图
假设第一次请求一个资源,返回 header 里面带上如下字段:
Cache-Control: max-age=600
Last-Modified: Wed, 28 Aug 2013 10:36:42 GMT
ETag: "124752e0d85461a16e76fbdef2e84fb9"
抛开细枝末节的东西,那么第二次请求通常大致流程图如下:
当前资源缓存是否过期:response_is_fresh = (freshness_lifetime > current_age)
|
-----------------------------------
| |
是 否
| |
发送请求,带上请求头 从本地缓存中获取资源(不发请求)
If-Modified-Since: 此资源Last-Modified的值
If-None-Match: 此资源ETag的值
|
服务器根据 If-Modified-Since 和 If-None-Match
两个值判断资源是否更新过
|
-------------------------
| |
是 否
| |
返回一个 status code:200 的 response 返回一个 status code:304 的 response
response body 里面是请求的资源 response body 为空
| |
浏览器用 response body里面的资源 依然从本地缓存里面获取资源
替换本地缓存中的资源
特殊情况
当你去浏览器验证的时候可能会碰到一些特殊情况,就是缓存有效,但是你刷新浏览器依然发送的条件请求。其实是因为浏览器在请求头中加入了一些料,譬如: Cache-Control: max-age=0。你刷新的方式可以有很多种,譬如:按F5,按ctrl+F5,在地址栏按回车等等。这些不同的行为都会影响浏览器发送请求的行为。这里有一些参考《在浏览器地址栏按回车、F5、Ctrl+F5刷新网页的区别》