一文让你学会使用http缓存

620 阅读9分钟

http缓存

http缓存分为强缓存协商缓存

强缓存: Cache-ControlExpires

协商缓存:ETagLast-Modified

二、http缓存策略的命中过程

下面是HTTP缓存策略的命中过程和优先级的一般流程:

  1. 浏览器发起请求: 当浏览器需要请求一个资源时,它会向服务器发送一个HTTP请求。
  2. 检查强制缓存: 浏览器首先会检查缓存是否有强制缓存副本。它会查看响应的缓存头字段,如Cache-ControlExpires。如果资源在缓存有效期内(根据max-ageExpires字段),浏览器直接从缓存中获取资源,不会向服务器发起请求。
  3. 检查协商缓存: 如果资源不在强制缓存有效期内,浏览器会向服务器发送一个条件请求,带上缓存相关的标识,如ETagIf-Modified-Since。服务器会检查这些标识,如果资源未发生改变,服务器返回一个状态码为304(Not Modified)的响应,告诉浏览器可以使用缓存副本。浏览器接收到304响应后,从缓存中获取资源。
  4. 获取完整的响应: 如果资源未命中强制缓存和协商缓存,或者缓存副本失效,服务器会返回一个完整的响应,包含新的资源内容。浏览器将接收到的响应存储在缓存中,并将其用于未来的请求。

image.png

三、Cache-contorl

以下是一些常见的 Cache-Control 指令值及其含义:

  • no-store:禁止缓存请求和响应的任何部分,每次都要向服务器发送请求。
  • no-cache:必须进行协商缓存验证,缓存可以使用已经缓存的响应,但必须与服务器进行确认以确保它仍然有效。
  • public:响应可以被任何缓存(包括客户端和代理服务器)缓存。
  • private:响应只能被单个用户缓存,通常不会被共享缓存(如代理服务器)缓存。
  • max-age=<seconds>:指定资源在被认为过期之前可以被缓存的最长时间,单位为秒。

接下来用koa框架来展示如何使用cache-contorl,需要使用到两个文件index.html和server.js

index.html

<!DOCTYPE html>
<html>

<head>
    <title>API 请求示例</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>

<body>
    <h1>API 请求示例</h1>
    <button onclick="fetchData()">获取数据</button>
    <div id="response"></div>

    <script>
        function fetchData() {
            axios.get('http:localhost:3000/api/hello')
                .then(function (response) {
                    document.getElementById('response').innerText = response.data;
                })
                .catch(function (error) {
                    console.error(error);
                });
        }
    </script>
</body>

</html>

server.js

const Koa = require('koa');
const Router = require('koa-router');
const cors = require('koa2-cors');

const app = new Koa();
const router = new Router();

// 允许跨域请求
app.use(cors());

// 定义接口路由及处理逻辑
router.get('/api/hello', async (ctx) => {
    ctx.set('Cache-Control', 'max-age=3600');
    ctx.body = 'Hello, API!';
});

// 将路由中间件注册到 Koa 应用中
app.use(router.routes()).use(router.allowedMethods());

// 启动服务
app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

在上述示例中,通过 ctx.set() 方法设置了响应头中的 Cache-Control 字段为 'max-age=3600',表示资源在客户端的缓存有效期为 1 小时。这意味着浏览器在请求该资源后,会将其缓存并在接下来的 1 小时内直接使用缓存,而不会发起新的请求。

image.png

我们请求两次,就会发现第二次请求会出现from disk cache就表示命中缓存,从磁盘中返回存储的结果

Expires

Expires 的值是一个表示过期时间的日期字符串。这个日期字符串遵循 RFC 1123 格式

Expires: Wed, 01 Jun 2023 12:00:00 GMT

Expires 头字段的值是基于服务器的绝对时间,由于客户端和服务器之间的时间可能存在差异,因此更常见且推荐的是使用相对时间而不是绝对时间来控制缓存,例如使用 Cache-Control 头字段的 max-age 指令。

那么我们只需要把server.js改成下面的样子,为了和之前的区分我们把返回改成Hello, Expires!

const Koa = require('koa');
const Router = require('koa-router');
const cors = require('koa2-cors');

const app = new Koa();
const router = new Router();

// 允许跨域请求
app.use(cors());


// router.get('/api/hello', async (ctx) => {
//     ctx.set('Cache-Control', 'max-age=3600');
//     ctx.body = 'Hello, API!';
// });

router.get('/api/hello', async (ctx) => {
    // 设置过期时间为一天后
    const expirationDate = new Date();
    expirationDate.setDate(expirationDate.getDate() + 1);

    // 将过期时间设置到 Expires 头字段
    ctx.set('Expires', expirationDate.toUTCString());
    ctx.body = 'Hello, Expires!';
});



// 将路由中间件注册到 Koa 应用中
app.use(router.routes()).use(router.allowedMethods());

// 启动服务
app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

但是我们发现,当我们请求返回的还是Hello, API!,那是因为我们之前把cache-control设置了1小时,所以现在还是从缓存中拿到结果,并没有真正的请求服务器,

image.png

这时候我们只需要右击选择清空缓存并硬性重新加载,就可以把之前的缓存去掉了。

image.png

这时候再请求,发现返回的是Hello, Expires!,并且在第二次请求中会直接从缓存中返回结果

image.png

Cache-contorl与Expires区别

  • Cache-Control 是一个通用的缓存控制指令,可以在响应头中设置。它提供了更细粒度和灵活的缓存控制选项,例如指定缓存的最大有效时间、禁用缓存、要求验证等。

  • Expires 是一个指定资源过期时间的字段,以绝对时间的形式表示。服务器通过在响应头中设置 Expires 字段,告诉客户端在指定的时间之后,应该认为该资源已经过期。

  • 如果同时设置了 Cache-ControlExpiresCache-Control 的优先级更高,会覆盖 Expires 的设置。在现代的 Web 开发中,Cache-Control 更常用,因为它提供了更好的灵活性和可靠性。

ETag

ETag 的取值可以是任意字符串,通常由服务器生成并与资源相关联。常见的取值方式有以下几种:

  1. 基于内容的哈希值:服务器可以使用哈希算法(如 MD5 或 SHA)对资源内容进行计算,生成唯一的哈希值作为 ETag 的取值。例如,ETag: "d41d8cd98f00b204e9800998ecf8427e"
  2. 版本号:如果资源有版本控制机制,服务器可以将资源的版本号作为 ETag 的取值。例如,ETag: "v1.2.3"
  3. 时间戳:服务器可以使用资源的最后修改时间或其他时间信息作为 ETag 的取值。例如,ETag: "1622781692"

客户端在发送条件请求时,可以使用 If-None-Match 请求头字段将之前获取的 ETag 值发送给服务器,服务器会根据这个值来判断资源是否发生了变化。如果 ETag 值匹配,则服务器返回 304 Not Modified 状态码,表示客户端的缓存仍然有效,可以继续使用缓存的资源。如果 ETag 值不匹配,服务器返回新的资源并带有新的 ETag 值。

我们基于server.js的基础上添加一个新的接口

const etag = require('etag');

router.get('/api/etag', async (ctx) => {
    // 生成资源内容
    const resourceContent = 'hello Etag';

    // 生成 ETag
    const resourceETag = etag(resourceContent);

    // 设置 ETag 到响应头
    ctx.set('ETag', resourceETag);

    // 检查客户端发送的 If-None-Match 请求头
    const clientETag = ctx.get('If-None-Match');
    if (clientETag === resourceETag) {
        // ETag 匹配,返回 304 Not Modified
        ctx.status = 304;
    } else {
        // ETag 不匹配,返回新的资源内容
        ctx.body = resourceContent;
    }
});

在处理请求时,我们首先从请求头中获取客户端发送的 If-None-Match 值,然后将其与服务器生成的 ETag 值进行比较。如果两者匹配,表示客户端的缓存仍然有效,服务器返回 304 Not Modified 状态码。如果不匹配,表示客户端的缓存已过期或不存在,服务器返回新的资源内容。

Last-Modified

Last-Modified的值是一个表示日期和时间的字符串,格式为 HTTP-date(例如:Wed, 21 Oct 2015 07:28:00 GMT)。

Last-Modified 响应头通常与条件请求一起使用,以便客户端在下次请求资源时与服务器验证资源是否发生了修改。客户端可以将之前获取到的 Last-Modified 值通过请求头的 If-Modified-Since 字段发送给服务器,服务器将根据该值来判断资源是否被修改过。

我们基于server.js的基础上添加一个新的接口

let resource = {
    id: 1,
    content: 'hello lastModified',
    lastModified: new Date().toUTCString()
};

router.get('/api/last-modified',async (ctx) => {
    const ifModifiedSince = ctx.get('if-modified-since');

    if (ifModifiedSince && new Date(ifModifiedSince) >= new Date(resource.lastModified)) {
        // 如果资源未被修改,返回 304 Not Modified 状态码
        ctx.status = 304;
    } else {
        // 如果资源已被修改或是首次请求,返回新的响应
        ctx.set('Last-Modified', resource.lastModified);
        ctx.body = resource.content
    }
});

服务器会检查请求头中的 If-Modified-Since 字段的值。如果这个时间戳大于或等于资源的最后修改时间,服务器会返回 304 Not Modified 状态码,表示资源未被修改,客户端可以使用缓存的响应。否则,服务器会返回新的响应,同时在响应头中设置 Last-Modified 字段,表示资源的最后修改时间。

Etag与Last-Modified的区别

  • ETag 是一个由服务器生成的唯一标识符,而 Last-Modified 是表示资源最后修改时间的时间戳。
  • ETag 更精确,能够准确识别资源的变化,不仅仅依赖于最后修改时间。
  • Last-Modified 只能以秒为单位进行比较,而 ETag 没有这个限制。
  • 在某些情况下,由于文件系统时间戳的精度问题,Last-Modified 可能不够可靠。
  • ETag 可以在服务器生成响应时进行更复杂的计算,例如使用哈希算法,以确保唯一性。
  • 优先级方面,ETag 的优先级高于 Last-Modified。当客户端发送了 If-None-Match 头字段时,服务器会优先比较 ETag 值,如果匹配,则返回 304 Not Modified 状态码。只有在 ETag 值不匹配的情况下,服务器才会继续比较 Last-Modified 值。 在实际应用中,通常会同时使用 ETag 和 Last-Modified 进行协商缓存,以充分利用它们各自的优势,并提高缓存的精确性和效率。

写在最后

如果这篇文章对你有帮助,记得点赞和收藏哦!你的支持是对我的最大鼓励和肯定。

同时,如果你有任何意见、建议或者想与我分享你的经验,欢迎在评论区留言,让我们一起互动和交流。我非常期待听到你的声音。

再次感谢你的阅读和支持!希望这篇文章能够给你带来真正的价值,帮助你解决问题或者提供新的见解。

谢谢!