流程图
优点
缺点
强缓存
适用于有哈希值的文件,比如 theme.ajdb35k.css
如果浏览器判断请求的目标资源有效命中强缓存,则可以直接从内存中读取目标资源,无需与服务器做任何通讯。
基于 Expires 字段实现的强缓存
以前,我们通常会使用响应头的Expires
字段去实现强缓存。Expires
字段的作用是,设定一个强缓存时间。在此时间范围内,则从内存(或磁盘)中读取缓存返回,不会去服务器请求。
但是,Expires
已经被废弃了。对于强缓存来说,Expires 已经不是实现强缓存的首选。
废弃的原因
Expires
判断强缓存是否过期的机制是获取本地时间戳与 Expires
的设置的时间做对比,如果本地时间有问题,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。
只有在考虑向下兼容的时候才使用 Expires
基于 Cache-control 实现的强缓存
Cache-control
字段在 http1.1 中被增加,用于代替 Expires 方法。
Cache-control 的使用方法页很简单,只需在资源的响应头上写上缓存时间(单位秒)。比如↓Cache-Control: max-age = 10
表示从第一次返回的时候开始,往后的 10 秒钟内如果该资源被再次请求,则从缓存中读取。Cache-control
共有六个字段
max-age | 客户端资源被缓存时长 |
---|---|
s-maxage | 代理服务器缓存的时长,必须和 public 一齐使用 |
no-cache | 跳过强缓存的校验,强制进行协商缓存 |
no-store | 禁止任何缓存 |
public | 既可以被浏览器缓存也可以被代理服务器缓存 |
private | 只能被浏览器缓存 |
no-cache
与no-store
互斥,不能同时存在private
与public
互斥,不能同时存在,若这两个属性都未被设置则默认为private
同时设置多个值:Cache-control:max-age=10000,s-maxage=200000,public
协商缓存
基于last-modified的协商缓存
实现步骤
首次请求
- 在服务器端读取文件修改时间
- 将读取的修改时间赋给响应头的
last-modified
字段 - 设置
Cache-control: no-cache
之后的请求
- 在服务器端读取文件修改时间
- 读取
if-modified-since
响应头时间与文件修改时间进行比对 - 若一致,则返回 304,直接使用本地缓存
- 若不一致,则将新的修改时间赋给响应头的
last-modified
字段,设置Cache-control: no-cache
并返回新文件
app.get('/cache', (req, res) => {
const data = fs.readFileSync('../img/home/1.jpg')
const { mtime } = fs.statSync('../img/home/1.jpg') // 读取文件修改时间
// 读取上一次返回给客户端的文件修改时间
const ifModifiedSince = req.headers['if-modified-since']
// 如果一致则说明文件没有被修改,返回 304
if(ifModifiedSince === mtime.toUTCString()) {
res.statusCode = 304
res.end()
return
}
res.setHeader('last-modified', mtime.toUTCString()) // 设置文件最后修改时间到响应头 last-modified 字段
res.setHeader('Cache-Control', 'no-cache') // 开启协商缓存
res.end(data)
})
当客户端读取到 last-modified
的时,会在下次的请求头中携带If-Modified-Since
字段If-Modified-Since
就是服务器上一次返回给客户端的文件修改时间,即last-modified
里的时间
之后每次对该资源的请求都携带If-Modified-Since
字段,服务端将这个时间与文件的修改时间进行比对来决定是读取缓存还是返回新的资源。
缺陷
- 因为根据文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
- 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改,文件修改时间就不会发生改变,即使文件内容修改了,依然不会返回新的文件。
基于 ETag 的协商缓存
ETag 就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹
文件指纹:根据文件内容计算出的唯一哈希值。文件内容改变则指纹改变。
实现步骤
首次请求
- 在服务器端读取文件并计算文件指纹
- 将文件指纹在响应头的
etag
字段中跟资源一起返回给客户端 - 设置
Cache-control: no-cache
之后的请求
- 在服务器端读取文件并计算文件指纹
- 读取
if-none-match
响应头指纹与文件当前指纹进行比对 - 若一致,则返回 304,直接使用本地缓存
- 若不一致,则将新的指纹赋给响应头的
etag
字段,设置Cache-control: no-cache
并返回新文件
app.get('/cache', (req, res) => {
const data = fs.readFileSync('../img/home/1.jpg')
const etagContent = etag(data) // 生成文件指纹
// 读取上一次返回给客户端的文件指纹
const ifNoneMatch = req.headers['if-none-match']
// 如果一致则说明文件没有被修改,返回 304
if(ifNoneMatch === etagContent) {
res.statusCode = 304
res.end()
return
}
res.setHeader('etag', etagContent) // 设置文件指纹到响应头 etag 字段
res.setHeader('Cache-Control', 'no-cache') // 开启协商缓存
res.end(data)
})
当客户端读取到 etag
的时,会在下次的请求头中携带If-None-Match
字段If-None-Match
就是服务器上一次返回给客户端的文件指纹,即etag
里的指纹
之后每次对该资源的请求都携带If-None-Match
字段,服务端将这个指纹与文件当前指纹进行比对来决定是读取缓存还是返回新的资源。
缺陷
- ETag 需要计算文件指纹这样意味着服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么 ETag 的计算就会影响服务器的性能。显然,ETag 在这样的场景下就不是很适合。
- ETag有强验证和弱验证,所谓将强验证,ETag 生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag 还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高,会降低协商缓存的有效性。
到底是用 ETag 还是 last-modified 完全取决于业务场景,这两个没有谁更好谁更坏
如何判断强缓存还是协商缓存
所有带304的资源都是协商缓存
所有标注(从内存中读取/从磁盘中读取)的资源都是强缓存
区别
强缓存与协商缓存获取资源的方式都是直接从本地缓存中获取,区别在于强缓存不会与服务器进行通信,直接使用缓存;而协商缓存需要与服务器进行一次通信来确定本地的缓存是否有效,如果有效则直接使用本地缓存,否则直接从服务器从新获取。