实现:HTTP缓存的使用及 demo

用 koa 来 demo 一下 HTTP 缓存的使用。

代码在这里 https://github.com/laputaz/bower-cache-demo

介绍

HTTP 缓存一般分为两类:强制缓存(也称本地缓存)和协商缓存(也称弱缓存)。

  • 本地缓存
    浏览器发送请求前,会先去缓存里查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。否则,进入下一步。

  • 协商缓存
    当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回 304 响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从浏览器缓存文件中获取这个资源。如果本地缓存和协商缓存都没有命中,则从直接从服务器加载资源。

战前准备

先起一个基础项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.js 入口
const Koa = require('koa')
const app = new Koa()
const routes = require('./routes.js').routes

app.use(routes)

// 公共,均返回文件
const base = async (ctx) => {
ctx.response.type = 'text/html'
ctx.response.body = `<h1>Hello, ${ctx.path.slice(1)}</h1>`
}
// 对于任何请求,app将调用该异步函数处理请求:
app.use(base)
// 在端口3000监听:
app.listen(3000)
1
2
3
4
5
6
7
8
// router.js
const Router = require('koa-router')
const router = new Router()

// 这里边写路由
// router.get('test-expires', (ctx) => {})

exports.routes = router.routes()
1
2
# 跑它
nodemon app.js

强制缓存 - Expires

Expires 响应头包含日期/时间(格林威治时间 GMT)在此时间之后,缓存过期。
无效的日期,比如 0, 代表着过去的日期,即该资源已经过期。
Expires 是 HTTP1.0 出现的, 优先级低于 Cache-Control。

Expires 有 3 个弱点。

  • 服务器端的时钟必须和客户端一致。
  • 最小的颗粒度为 s。
  • 由于是一个绝对的时间值,所以必须时常更新它的值。

格式

1
Expires: Sun, 26 Sep 2021 16:28:00 GMT

实现

1
2
3
router.get('test-expires', (ctx) => {
ctx.set('Expires', 'Sun, 26 Sep 2021 16:39:00 GMT')
})

效果
在这里插入图片描述

再次打开可以看到,读的是 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
2
3
4
Cache-Control: max-age=20000 // 20s后过期
Cache-Control:no-cache // 会缓存,但是否使用需要请求服务器确认
Cache-Control:no-store // 不允许所有缓存
Cache-Control: private // 只有浏览器可缓存,代理服务器不行

效果
在这里插入图片描述

再次打开可以看到,读的是 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。

在这里插入图片描述