用 koa 来 demo 一下 HTTP 缓存的使用。
代码在这里 https://github.com/laputaz/bower-cache-demo
介绍
HTTP 缓存一般分为两类:强制缓存(也称本地缓存)和协商缓存(也称弱缓存)。
本地缓存
浏览器发送请求前,会先去缓存里查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。否则,进入下一步。协商缓存
当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回 304 响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从浏览器缓存文件中获取这个资源。如果本地缓存和协商缓存都没有命中,则从直接从服务器加载资源。
战前准备
先起一个基础项目
1 | // app.js 入口 |
1 | // router.js |
1 | # 跑它 |
强制缓存 - Expires
Expires 响应头包含日期/时间(格林威治时间 GMT)在此时间之后,缓存过期。
无效的日期,比如 0, 代表着过去的日期,即该资源已经过期。
Expires 是 HTTP1.0 出现的, 优先级低于 Cache-Control。
Expires 有 3 个弱点。
- 服务器端的时钟必须和客户端一致。
- 最小的颗粒度为 s。
- 由于是一个绝对的时间值,所以必须时常更新它的值。
格式
1 | Expires: Sun, 26 Sep 2021 16:28:00 GMT |
实现
1 | router.get('test-expires', (ctx) => { |
效果
再次打开可以看到,读的是 disk cache 磁盘缓存。
强制缓存 - Cache-Control
Cache-Control 是 HTTP1.1 提出的。提供了比 Expires 更精细的缓存时间,毫秒级。以及可以设置不缓存。
常用值:
- 可缓存性
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,Cache-Control 的默认取值
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定(名字容易误导)
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- 到期
- max-age=xxx 毫秒:缓存内容将在 xxx 秒后失效
指定 no-cache 或 max-age=0, must-revalidate 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。
格式
1 | Cache-Control: max-age=20000 // 20s后过期 |
效果
再次打开可以看到,读的是 disk cache 磁盘缓存。
协商缓存 - Last-Modified / If-Modified-Since
If-Modified-Since 则是客户端发起该请求时,携带上一次响应头返回的 Last-Modified 值,告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200;否则则返回 304,代表资源无更新,可继续使用缓存文件。
未设置的情况,响应头没有 Last-Modified,请求头当然也没有 If-Modified-Since
响应头设置 Last-Modified,并第一次请求, 此时请求头当然也没有 If-Modified-Since,这时,文件已经被浏览器缓存了。
1
2
3
4
5
6
7// last-modified
router.get('/test-last-modified', (ctx, next) => {
ctx.set('Cache-Control', 'no-cache')
// 设置 Last-Modified
ctx.set('Last-Modified', 'Sun, 26 Sep 2021 21:39:03 GMT')
next()
})下一次请求,可以看到,请求头已经有 If-Modified-Since 了
请求过来时,服务端会对请求头中的 If-Modified-Since 与文件修改时间作对比,这就是代码上控制了。这里假设文件没过期,返回 304。
1
2
3
4
5
6
7
8
9
10
11
12
13// last-modified
router.get('/test-last-modified', (ctx, next) => {
ctx.set('Cache-Control', 'no-cache')
// 对请求头中的 If-Modified-Since 与文件修改时间做对比, 这里不写了
if (true) {
// 发现文件时间未更新,返回 304,让浏览器读缓存。
ctx.response.status = 304
} else {
// 发现文件时间已更新,更新 Last-Modified
ctx.set('Last-Modified', 'Sun, 28 Sep 2021 21:39:03 GMT')
}
next()
})
协商缓存 - Etag / If-None-Match
Last-Modified 与 Expires 一样,是基于绝对时间的,一样存在客户端和服务端可能时间不一致的问题。
Etag 与 Last-Modified 使用类似,只是不使用时间作为标识,而是使用一个标记。
未设置的情况,响应头没有 Etag,请求头当然也没有 If-None-Match
响应头设置 Etag,并第一次请求, 此时请求头当然也没有 If-None-Match。这时,文件已经被浏览器缓存了。
1
2
3
4
5
6
7// etag
router.get('/test-etag', (ctx, next) => {
ctx.set('Cache-Control', 'no-cache')
// 设置 Etag
ctx.set('Etag', '1234')
next()
})下一次请求,可以看到,请求头已经有 If-None-Match 了
请求过来时,服务端会对请求头中的 If-None-Match 该资源在服务器的 Etag 值作对比,这就是代码上控制了。这里假设文件没过期,返回 304。
1
2
3
4
5
6
7
8
9
10
11
12// etag
router.get('/test-etag', (ctx, next) => {
ctx.set('Cache-Control', 'no-cache')
// 对请求头中的 If-None-Match 与文件标识作对比,
if (true) {
// 发现文件时间未更新,返回 304,让浏览器读缓存。
ctx.response.status = 304
} else {
// 发现文件时间已更新,更新 Etag
ctx.set('Etag', '1234')
}
})
以上
强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存。强制缓存生效时,在 devtool 看到的结果是 200。协商缓存则是 304。