前言
HTTP缓存在Web开发中扮演着重要角色,可以显著提升网站的性能和用户体验。
本文将介绍
- http缓存介绍及工作原理
- 常见的缓存策略
- 使用原生http进行服务端模拟
1. 什么是HTTP缓存
HTTP缓存是一种机制,用于在客户端(例如浏览器)和服务器之间存储和复用资源的副本,以减少网络请求和提高性能。通过使用HTTP缓存,浏览器可以避免重复下载相同的资源,并直接从本地缓存加载数据。
2. HTTP缓存的工作原理
当浏览器首次请求一个资源时,服务器会返回该资源的响应,并在响应头部中包含缓存相关的信息。浏览器会根据这些信息决定是否应该将该资源缓存起来,以及在未来的请求中如何使用缓存。
当再次请求同一个资源时,浏览器会检查缓存相关的响应头字段,并与本地缓存进行比较。如果满足缓存条件(如缓存未过期或资源未发生改变),浏览器会直接从缓存中加载资源,而无需向服务器发起请求。
如图所示:
3. 常见的HTTP缓存策略
3.1 强缓存
强缓存通过设置 Cache-Control 和 Expires 设置。
它使得在缓存有效期内浏览器可以直接加载资源,而不需要向服务器发送请求。
3.2 协商缓存
协商缓存可通过 Cache-Control和Pragma设置。
当缓存失效时,浏览器可以发送一个验证请求到服务器,通过比较实体标签(ETag)或者最后修改时间(Last-Modified)来判断资源是否有更新。
如果服务器返回状态码为 304 Not Modified,则表示资源未发生变化,浏览器可以直接从缓存加载资源,减少网络传输。
3.3 缓存控制策略
| 响应头 | 属性值 | 优先级 | 解释 |
|---|---|---|---|
| Expires | 以GMT(格林尼治标准时间)格式表示的日期(toUTCString方法转换) | 低 | HTTP/1.0中的属性 会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。 |
| Pragma | no-cache | 高 | HTTP/1.0中的属性 效果和 Cache-Control 中的 no-cache 一致,使用协商缓存 |
| Cache-Control | max-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,每次刷新都是重新请求,响应头没有任何缓存设置。
服务端日志:
4.2.2 设置强缓存
- 使用Expires设置
app.js
res.writeHead(200, {
Expires: new Date(
"2023-08-18 11:24:55"
).toUTCString(),
});
}
未过期:从缓存读取,服务器没有请求到
第一次请求后,第二次图片缓存了服务器没发送请求
时间过期:
重新向服务器发送请求
问题:如果服务器和客户端时间不同步,就会出现缓存失效问题?
- 使用Cache-Control设置
为了解决时间不同步问题,http中新增了max-age。具体原理是:客户端收到响应,过期时间以客户端时间加上max-age时间计算得到,避免了时间不同步问题。
res.writeHead(200, {
"Cache-Control": "max-age=30", // 滑动时间单位s
});
设置过期时间30s,第一个请求:
当前Date时间没有超过30s,未过期,从浏览器内存读取
当超过30s后重新请求
服务端请求详情:
4.2.3 设置协商缓存
为了看出效果,我们将强缓存进行设置对比。
- 使用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优先级大于Expires和Cache-Control。
- 使用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
通过上面的设置,强缓存失效,那可能会有疑惑,我设置了协商缓存为什么没有缓存呢?
因为服务端需要选择是使用
ETag还是Last-Modified方式对比,对比成功需要设置响应头为304,这样浏览器才能正确的缓存。接下来进行服务器响应头ETag还是Last-Modified设置。
- 协商缓存-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);
第一次请求:
第二次请求,自动添加if-Modified-Since请求头,服务端对比无变化返回304
- 协商缓存-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);
第二次请求,自动添加If-None-Match
- 两者同时使用
优先使用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);
5. 总结
通过实际的案例去理解强缓存和协商缓存的运行机制,响应头设置的优先级。希望能够对你理解和应用HTTP缓存有所帮助。如有错误,请指正O^O。