缓存实践
在缓存的介绍中,这位大佬的文章已经有非常详细的介绍 浏览器缓存策略之扫盲篇,而且文章还附有相应的代码,但我在阅读的过程中出现一些问题和一些思考,因此就按照大佬的文章再次验证和补充了一些
缓存
有些介绍 HTTP 的书会简单介绍一下缓存,像图解 HTTP 的缓存一章在 5.3 和 6.3 中有相关的介绍,但是都是一些理论知识,实际上看完之后也不知道怎么去用?所以还得去查一下相关的资料来实践一下。
首先我们要知道浏览器下访问资源的方式,下面是歪马大佬写的 浏览器缓存策略之扫盲篇 的介绍内容
浏览器下访问资源的方式主要有以下 7 种:
- (新标签)地址栏回车
- 链接跳转
- 前进、后退
- 从收藏栏打开链接(window.open)新开窗口
- 刷新(Command + R / F5)
- 强制刷新(Command + Shift + R / Ctrl + F5)
使用这 7 种方式访问资源时,应用缓存的策略会有一些不同。如下图所示。通过上述 7 种方式访问资源,会从不同的缓存应用判断步骤开始。此处不做验证,相信大家看了后面的内容,能够自行验证的。
需要注意的是,Chrome 中在当前地址栏,不改变内容,直接回车,等同于刷新当前页,而在 Firefox 下与其他在地址栏回车一样。这一点比较特殊,需要适当区分下。
注:本文配有测试脚本,代码在github上。下文会按照测试脚本进行述说,使用说明见下载链接。验证上述内容,可以执行 node cache-ETag+max-age.js,会同时开启 ETag 和 max-age,然后触发相应的动作,通过 Network 面板和 node 日志即可验证,此处篇幅有限先不赘述。
此外,这里提一个概念,webkit 资源分为主资源和派生资源。主资源是地址栏输入的 URL 请求返回的资源,派生资源是主资源中所引用的 JS、CSS、图片等资源。
在 Chrome 下刷新时,只有主资源的缓存应用方式如上图所示,派生资源的缓存应用方式与新标签打开类似,会判断缓存是否过期。强缓存生效时的区别在于新标签打开为 from disk cache,而当前页刷新派生资源是 from memory cache。
而在 Firefox 下,当前页面刷新,所有资源都会如上图所示。下文也会利用 Chrome 的这一特点在当前页刷新,派生资源会使用缓存进行测试。不然每次都需要打开新标签较为繁琐。
Cache-Control:协商缓存
Cache-Control: cache-directive[,cache-directive]
cache-directive 缓存指令,[,cache-directive] 指还可以添加更多指令,比如说 cache-directive,cache-directive…
协商缓存大多用于时间在一定范围内缓存
max-age/Expires
max-age 是以秒(s)来作为计量单位,比如 max-age=60 即缓存时间为 1s。
Expires 有相同功能,但是 max-age > Expires
图文详解可参考,浏览器缓存策略之扫盲篇
注意
使用 github 上的使用样例时,如果出现结果并没有如同文章结果相同,可能是 Chrome 进行了相应的缓存,可以在 F12->Application->Cache->Cache Storage 右键刷新缓存(Refresh Caches),然后再次尝试。
注意
node 使用 github 样例时,如果出现以下错误
// Expires 字段得到了不合法的字符,就是往 Expires 里面写入了不合适的数据
UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["Expires"]
可能是 new Date(); // 字符串形式:Sat Oct 30 2021 00:43:32 GMT+0800 (中国标准时间)
Expires 无法处理中文,可以直接往 Expires 中填入 Sat Oct 20 2022 00:00:00 GMT+0800 (CST) 来解决这个问题或者使用 new Date().toUTCString()
no-store
请求和响应都可以加上,设置了 Cache-Control: no-store,请求的文件就都不会缓存了,但是浏览器禁用缓存的时候并不是使用的 no-store 而是 no-cache + pragma 的组合
no-cache/Pragma
http1.0 字段, 通常设置为 Pragma:no-cache, 作用与 Cache-Control:no-cache 相同。当在浏览器进行强刷(Comand + Shift + R / Ctrl + F5)或在 NetWork 面板内勾选禁用缓存(Disable Caches)时,会自动带上 Pragma:no-cache 和 Cache-Control:no-cache,并且不会带上协商策略中所涉及的信息(下面介绍的 If-Modified-Since/If-None-Match)。这是不会使用任何缓存,重新获取资源。下面会有相应的图介绍。
Last-Modified/If-Modified-Since
服务器可以设置文件修改时间(比如图片的修改时间)为 Last-Modified,浏览器中会缓存发送过来的 Last-Modified,下次请求(比如刷新)时会携带该值作为 If-Modified-Since 给服务器,而服务器判断该值与当前文件的修改时间是否相同
- 如果不同,说明文件已经更新,那就重发
- 如果相同,说明文件还没有改变,返回状态码 304,让浏览器使用自己的缓存
具体效果如下
第一次请求,服务器发送回来一个红色的照片,服务器响应头会带有 Last-Modified 的值
再次刷新请求一次(ctrl+R)
状态码是 200 OK (from memory cache),说明图片是从缓存中读取的,它的加载时间也是 0 ms,你可能注意到了
- 没有请求头
- 也没有按照代码中的那样返回 304
- 请求头那里有 Provisional headers are shown. Disable cache to see full headers.
这是因为我们没有走网络请求而是本地服务器(但已经达到了我们缓存的目的),下面是 google 解释的原文
It could be due to the request not sent over the network (served from a local cache), which doesn’t store the original request headers. In this case, you can disable caching to see the full request headers.
如果你想要认真验证的话,也可以去 Firefox 中验证,结果如下图
这个图就能够看到浏览器中缓存的 If-Modified-Since 和服务器返回的 304 状态码
到了这里其实还有一个问题,如果我的浏览器是默认不缓存的会怎么样?(比如某些移动端浏览器,用户可以去禁止缓存)发出的请求还会携带 If-Modified-Since 吗?
还是以火狐浏览器(Firefox)为例
这个时候无论你刷新多少次,请求头都不会携带 If-Modified-Since,包括下面介绍的 ETag 也不会携带
注:在实际测试中可能会发向浏览器实际效果与文章效果不同,我在测试也遇到了这个问题,比如设置了 Etag,但刷新后请求头就是不带 If-Not-Match,这种情况可以关掉 Chrome 重启,或者使用 Firfox 进行测试
If-Unmodified-Since
从字面上看, 意思是: 如果从某个时间点算起, 文件没有被修改…
- 如果没有被修改: 则开始`继续’传送文件: 服务器返回: 200 OK
- 如果文件被修改: 则不传输, 服务器返回: 412 Precondition failed (预处理错误)
用途: 断点续传(一般会指定 Range 参数). 要想断点续传, 那么文件就一定不能被修改, 否则就不是同一个文件了, 断续还有啥意义?
我没有找到合适的方法去测试 If-Unmodified-Since
ETag/If-None-Match
由下图可以看到,当我们替换图片的时候,ETag 有了明显的变化
其中 Request Headers 是刷新时,浏览器向服务器中请求的请求头,可以看到当时的 If-None-Match 是 “0dbc…”
而服务器这边我们已经修改了图片,所以图片的 md5 已经改变了,所以发送回来的 ETag 也与浏览器先前缓存中的不同,为 “3808…”
服务器中打印的日志也可以验证
如果这个时候我们再次刷新,此时服务器响应的 Status Code 会是 304,因为浏览器中已经更新了缓存中的 ETag
具体实现可以看源码,非常简单
注:文章中的 ETag 使用 md5 生成