协商缓存介绍(知道的可以跳过)
协商缓存是HTTP缓存机制的一种,当强缓存失效时,浏览器会向服务器发送请求验证资源是否更新。以下是协商缓存的核心组件和工作原理:
1. Last-Modified 与 If-Modified-Since
-
Last-Modified :服务器在响应头中返回资源的最后修改时间(精确到秒级)。
示例:
Last-Modified: Thu, 27 Feb 2025 18:06:00 GMT
。 -
If-Modified-Since :浏览器再次请求时,将上次收到的
Last-Modified
值通过此请求头发送给服务器。服务器对比当前资源修改时间,若未变化则返回304 Not Modified
,否则返回新资源 。
局限性:
-
秒级精度可能导致1秒内多次修改无法检测。
-
分布式系统中需确保多台服务器的文件修改时间一致。
2. ETag 与 If-None-Match
-
ETag :服务器生成的资源唯一标识符(如哈希值),优先级高于
Last-Modified
。示例:
ETag: "d3f4e5kamanotes"
。 -
If-None-Match :浏览器携带上次的
ETag
值发送请求。服务器比对当前资源ETag
,匹配则返回304
,否则返回新资源。
优势:
-
内容变化即触发更新,解决
Last-Modified
的精度问题。 -
适用于多语言或动态内容(如Session不同导致相同URL返回不同内容)。
注意:
- 分布式系统中
ETag
可能因服务器差异导致不一致,需谨慎配置 。
工作流程
-
首次请求 :服务器返回资源,并附带
Last-Modified
或ETag
。 -
再次请求 :浏览器发送
If-Modified-Since
或If-None-Match
,服务器验证后返回304
或新资源。 -
优先级 :若两者同时存在,
ETag
优先 。
应用场景
-
强缓存失效后 (如
max-age
过期或no-cache
)触发协商缓存。 -
动态资源(如API数据)适合用
ETag
,静态资源(如CSS/JS)可结合Last-Modified
。
通过协商缓存,有效减少带宽消耗和服务器压力,提升用户体验
代码实现
看得比较多,但是实际并没有试过实践,所以记录一下协商缓存实践
前端部分:
javascript
const getResource=()=>{
fetch('http://localhost:3000/resource')
.then(response => {
console.log('status:',response.status);
// 若未修改,返回304
if (response.status === 200) {
//一般接口返回304http状态码,会被浏览器自动转成200,document类型通常直接显示304;
console.log('请求数据');
return response.json();
}
else{
return '其他状态码';
}
})
.then(data=>{
console.log('log-zcw=>data',data);
})
}
服务端部分
javascript
import express from 'express';
const app = express();
const cors = require('cors');
const crypto = require('crypto');
// 全局使用 cors 中间件,允许所有来源的跨域请求
app.use(cors());
const mockData = {
id: 1,
content: "This is a sample resource",
updatedAt: new Date().toISOString() // 模拟最后修改时间
};
// 协商缓存(ETag + Last-Modified)
app.get('/resource', (req, res) => {
// 1. 生成ETag(基于对象内容的哈希)
const objString = JSON.stringify(mockData);
const etag = crypto.createHash('md5').update(objString).digest('hex');
res.setHeader('ETag', etag);//需要设置响应头,否则浏览器不会携带请求头if-none-match和if-modified-since
console.log('ETag',etag);
// 2. 设置Last-Modified(基于对象的更新时间)
const lastModified = new Date(mockData.updatedAt).toUTCString();
res.setHeader('Last-Modified', lastModified);//需要设置响应头,否则浏览器不会携带请求头match和if-modified-since
console.log('lastModified',lastModified);
// 检查客户端缓存验证头
const ifNoneMatch = req.headers['if-none-match'];//是上次返回的ETag值,浏览器会自动携带该请求头。
console.log('ifNoneMatch',ifNoneMatch);
const ifModifiedSince = req.headers['if-modified-since'];//是上次返回的Last-Modified值,浏览器会自动携带该请求头。
console.log('ifModifiedSince',ifModifiedSince);
//浏览器自动携带请求头【if-modified-since】,该行为是浏览器内置的HTTP协议实现,无需开发者干预
// 如果缓存有效,返回304
if (
(ifNoneMatch && ifNoneMatch === etag) ||
(ifModifiedSince && ifModifiedSince === lastModified)//如果资源的修改时间没修改,则返回304状态码和空响应体。
) {
console.log('缓存数据');
//HTTP状态码304表示资源未修改
res.status(304).end();//浏览器会自动转为200状态码,并从缓存中读取数据。
} else {
console.log('最新数据');
res.json(mockData); // 返回最新对象
}
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
运行后
客户端第一次在浏览器调用接口
服务端输出如下
客户端请求如下
客户端第二次在浏览器调用接口
服务端输出如下
客户端请求如下