http强缓存和协商缓存实践

1,393 阅读7分钟

前言

HTTP缓存在Web开发中扮演着重要角色,可以显著提升网站的性能和用户体验。

本文将介绍

  1. http缓存介绍及工作原理
  2. 常见的缓存策略
  3. 使用原生http进行服务端模拟

1. 什么是HTTP缓存

HTTP缓存是一种机制,用于在客户端(例如浏览器)和服务器之间存储和复用资源的副本,以减少网络请求和提高性能。通过使用HTTP缓存,浏览器可以避免重复下载相同的资源,并直接从本地缓存加载数据。

2. HTTP缓存的工作原理

当浏览器首次请求一个资源时,服务器会返回该资源的响应,并在响应头部中包含缓存相关的信息。浏览器会根据这些信息决定是否应该将该资源缓存起来,以及在未来的请求中如何使用缓存。

当再次请求同一个资源时,浏览器会检查缓存相关的响应头字段,并与本地缓存进行比较。如果满足缓存条件(如缓存未过期或资源未发生改变),浏览器会直接从缓存中加载资源,而无需向服务器发起请求。

如图所示:

image.png

3. 常见的HTTP缓存策略

3.1 强缓存

强缓存通过设置 Cache-ControlExpires 设置。

它使得在缓存有效期内浏览器可以直接加载资源,而不需要向服务器发送请求。

3.2 协商缓存

协商缓存可通过 Cache-ControlPragma设置。

当缓存失效时,浏览器可以发送一个验证请求到服务器,通过比较实体标签(ETag)或者最后修改时间(Last-Modified)来判断资源是否有更新。

如果服务器返回状态码为 304 Not Modified,则表示资源未发生变化,浏览器可以直接从缓存加载资源,减少网络传输。

3.3 缓存控制策略

响应头属性值优先级解释
Expires以GMT(格林尼治标准时间)格式表示的日期(toUTCString方法转换)HTTP/1.0中的属性
会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。
Pragmano-cacheHTTP/1.0中的属性
效果和 Cache-Control 中的 no-cache 一致,使用协商缓存
Cache-Controlmax-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效

no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜

no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源

private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应

public:响应可以被中间代理、CDN 等缓存

must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证
HTTP/1.1 中新增的属性
使用协商缓存主要设置 ETag由服务器生成的资源唯一标识符和Last-Modified资源的最后修改时间 进行校验资源是否发生了变化

4. http服务端模拟

4.1 创建目录

cache
├─app.js // 服务端程序
├─index.html // 首页
├─images // 资源图片文件
|   ├─img01.jpeg
|   ├─img02.jpeg
|   ├─img03.jpeg
|   └img04.jpeg

4.2 缓存代码实现

index.html:

使用图片进行缓存测试

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>浏览器缓存</title>
</head>
<body>
	<h1>http缓存</h1>
	<img src="./images/img01.jpeg" alt="" width="100" height="100" srcset="">
</body>
</html>

app.js:

服务端代码,通过图片的加载,设置不同缓存方式进行说明:

4.2.1 不使用任何缓存方式

const http = require("node:http");
const fs = require("node:fs");
// 启动一个服务
http.createServer((req, res) => {
    console.log(req.method, req.url); // 打印请求方式和url
    // 获取资源路径 例如http://localhost:3000/images/img01.jpeg =>获取到 /images/img01.jpeg
    const pathname = req.url;
    if (pathname === "/") {
     // 如果是根目录 返回读取html返回
      const html = fs.readFileSync("./index.html");
      res.end(html);
    } else if (pathname === "/images/img01.jpeg") {
      // 获取资源图片 得到进行返回展示
      const data = fs.readFileSync("./images/img01.jpeg");
      // 之后在这个地方写缓存设置,省略其他内容
      res.end(data);
    } else {
      // 其他状态404
      res.statusCode = 404;
      res.end;
    }
}).listen(3000, () => {
    console.log("http://localhost:3000");
});

访问http://localhost:3000,每次刷新都是重新请求,响应头没有任何缓存设置。

image.png

服务端日志:

image.png

4.2.2 设置强缓存

  1. 使用Expires设置

app.js

res.writeHead(200, {
    Expires: new Date(
      "2023-08-18 11:24:55"
    ).toUTCString(),
});
}

未过期:从缓存读取,服务器没有请求到

image.png

第一次请求后,第二次图片缓存了服务器没发送请求

image.png

时间过期:

image.png

重新向服务器发送请求

image.png

问题:如果服务器和客户端时间不同步,就会出现缓存失效问题?

  1. 使用Cache-Control设置

为了解决时间不同步问题,http中新增了max-age。具体原理是:客户端收到响应,过期时间以客户端时间加上max-age时间计算得到,避免了时间不同步问题。

res.writeHead(200, {
    "Cache-Control": "max-age=30", // 滑动时间单位s
});

设置过期时间30s,第一个请求:

image.png 当前Date时间没有超过30s,未过期,从浏览器内存读取

image.png 当超过30s后重新请求

image.png

服务端请求详情:

image.png

4.2.3 设置协商缓存

为了看出效果,我们将强缓存进行设置对比。

  1. 使用Pragma设置
res.setHeader("Pragma", "no-cache");
res.writeHead(200, {
    "Cache-Control": "max-age=30", 
    Expires: new Date(
      "2023-08-19 11:00:00"
    ).toUTCString(),
});

多次进行刷新,结果是每次都去服务器请求,可以看出Pragma优先级大于ExpiresCache-Control

image.png

  1. 使用Cache-Control设置
res.setHeader("Cache-Control", "no-cache");
res.writeHead(200, {
    "Cache-Control": "no-cache max-age=30", 
    Expires: new Date(
      "2023-08-18 11:24:55"
    ).toUTCString(),
});

多次刷新,每次去服务器请求。可以看出Cache-Control的优先级大于Expires

可以得到响应头优先级:Pragma > Cache-Control > Expires

image.png

通过上面的设置,强缓存失效,那可能会有疑惑,我设置了协商缓存为什么没有缓存呢?

因为服务端需要选择是使用ETag还是Last-Modified方式对比,对比成功需要设置响应头为304,这样浏览器才能正确的缓存。接下来进行服务器响应头ETag还是Last-Modified设置。

  1. 协商缓存-Last-Modified

Last-Modified是通过最后修改时间进行对比,对比成功返回304.

const data = fs.readFileSync("./images/img01.jpeg");
// 获取最新的修改时间
const { mtime } = fs.statSync("./images/img01.jpeg");
const lastModified = mtime.toUTCString();

// 设置协商缓存 两者都可以 此处选择Cache-Control
// res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache");
// 返回响应头最后修改时间
res.setHeader("last-modified", lastModified);

// 获取上次请求的最后修改时间
const ifModifiedSince = req.headers["if-modified-since"];
if (ifModifiedSince === lastModified) {
    res.statusCode = 304;
    res.end();
    return;
}
res.end(data);

第一次请求: image.png

第二次请求,自动添加if-Modified-Since请求头,服务端对比无变化返回304

image.png

  1. 协商缓存-ETag
const crypto = require("crypto");

const data = fs.readFileSync("./images/img01.jpeg");
const etag = crypto.createHash("md5").update(data).digest("hex"); // 生成etag

res.setHeader("etag", etag);
res.setHeader("Cache-Control", "no-cache");

const ifNoneMatch = req.headers["if-none-match"];
if (ifNoneMatch === etag) {
    res.statusCode = 304;
    res.end();
    return;
}
res.end(data);

image.png

第二次请求,自动添加If-None-Match

image.png

  1. 两者同时使用

优先使用etag,因为时间可以进行修改不准确,而etag是根据内容生成的。

  const data = fs.readFileSync("./images/img01.jpeg");
  // 获取etag
  const etag = crypto
    .createHash("md5")
    .update(data)
    .digest("hex");

  // 获取last-modified
  const { mtime } = fs.statSync("./images/img01.jpeg");
  const lastModified = mtime.toUTCString();

  // 设置响应头
  res.setHeader("last-modified", lastModified);
  res.setHeader("etag", etag);
  res.setHeader("Cache-Control", "no-cache");

  // if-none-match与etag对比
  const ifNoneMatch = req.headers["if-none-match"];
  if (ifNoneMatch === etag) {
    res.statusCode = 304;
    res.end();
    return;
  }

  // if-modified-since与lastModified
  const ifModifiedSince =
    req.headers["if-modified-since"];
  if (ifModifiedSince === lastModified) {
    res.statusCode = 304;
    res.end();
    return;
  }

  res.end(data);

image.png

5. 总结

通过实际的案例去理解强缓存和协商缓存的运行机制,响应头设置的优先级。希望能够对你理解和应用HTTP缓存有所帮助。如有错误,请指正O^O。


nginx如何设置缓存方式