核心机制:内容验证而非文件验证
协商缓存的核心是验证资源内容是否变化,而不是文件本身是否修改(例如文件元数据变化但内容未变时不应更新)
两种验证机制及优先级
-
ETag/If-None-Match(优先级更高)
-
工作原理:
-
服务器生成资源内容的哈希值(如
ETag: "33a64df55142f"
) -
浏览器后续请求携带
If-None-Match: "33a64df55142f"
-
服务器比较哈希值:
nginx
csharp# Nginx 自动启用 ETag(默认配置) etag on; # 验证逻辑伪代码 if request.headers['If-None-Match'] == generate_etag(current_content) { return 304; } else { return 200 with new content; }
-
-
优势:
- 精确感知内容变化(1字节变化也会使哈希值改变)
- 避免时间同步问题
- 处理文件重命名但内容不变的情况
-
-
Last-Modified/If-Modified-Since
-
工作原理:
-
服务器返回最后修改时间(如
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
) -
浏览器后续请求携带
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
-
服务器比较修改时间:
nginx
bash# 自动启用(默认) if_modified_since exact; # 精确模式 # 验证逻辑伪代码 if request.headers['If-Modified-Since'] >= resource.modified_time { return 304; } else { return 200; }
-
-
缺陷:
- 最小时间单位为秒,1秒内多次修改无法识别
- 文件内容未变但修改时间更新(如打包重建)会误判
- 服务器时间不同步导致问题
-
优先级规则
当两种机制同时存在时:

原因:ETag 基于内容哈希,比时间戳更可靠(符合 HTTP/1.1 规范 RFC 7232)
关键实践:内容变化检测的陷阱与解决方案
场景:内容未变但文件改变的案例
-
文件重建导致时间戳更新
-
问题:CI/CD 流水线每次构建都更新文件时间戳
-
解决方案:
nginx
csharp# 禁用 Last-Modified,强制使用 ETag add_header Last-Modified ""; etag on;
-
-
ETag 误判问题
-
问题:分布式服务器使用 inode 生成 ETag(不同节点的 inode 不同)
-
解决方案(Nginx):
nginx
perl# 关闭默认 ETag(包含 inode) etag off; # 自定义基于内容的 ETag add_header ETag "%X-Content-MD5"; # 需要安装 ngx_http_ssi_filter_module
-
高级内容验证策略
-
弱校验 ETag(W/ 前缀)
- 适用场景:内容语义不变的小修改(如空格调整)
- 服务器返回:
ETag: W/"33a64df55142f"
- 浏览器行为:接受弱 ETag 匹配的 304 响应
-
内容摘要验证
nginx
bash# 生成 SHA-256 内容摘要 add_header Digest 'sha-256=base64(sha256(content))';
- 浏览器通过
Want-Digest
头主动请求验证
- 浏览器通过
缓存配置最佳实践
强缓存 + 协商缓存组合策略
nginx
csharp
server {
# 带哈希的静态资源(强缓存)
location ~* .[a-f0-9]{8}.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
etag off; # 不需要协商缓存
}
# 无哈希资源(协商缓存)
location ~* .(js|css)$ {
add_header Cache-Control "public, max-age=0, must-revalidate";
etag on;
}
# HTML 文件(禁用缓存)
location /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
etag on;
}
}
动态内容缓存验证
javascript
javascript
// 前端主动验证内容更新
async function checkContentUpdate(url, storedETag) {
const response = await fetch(url, {
headers: { 'If-None-Match': storedETag },
cache: 'no-cache'
});
if (response.status === 304) {
console.log('内容未变化');
} else {
const newETag = response.headers.get('ETag');
// 存储新 ETag 并更新内容
}
}
验证工具与调试技巧
-
浏览器网络面板分析
- 304 响应:协商缓存生效
Provisional headers
:强缓存生效
-
CURL 手动验证
bash
bash# 首次获取 ETag curl -I https://example.com/app.js # 发送验证请求 curl -H 'If-None-Match: "33a64df55142f"' -I https://example.com/app.js
-
关键头信息
http
arduinoHTTP/1.1 304 Not Modified ETag: "33a64df55142f" Cache-Control: max-age=0, must-revalidate
特殊场景处理
-
Service Worker 的缓存验证
javascript
csharpself.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(cached => { return fetch(event.request).then(network => { // 比较 ETag if (cached && cached.headers.get('ETag') === network.headers.get('ETag')) { return cached; } return network; }).catch(() => cached); }) ); });
-
CDN 边缘计算验证
js
csharp// Cloudflare Worker 示例 addEventListener('fetch', event => { const request = event.request; if (request.headers.get('If-None-Match')) { // 直接比较 ETag 避免回源 const etagMatch = /* 从 KV 存储获取 ETag */; if (etagMatch) return new Response(null, { status: 304 }); } event.respondWith(handleRequest(request)); });
通过精确控制 ETag 生成策略和缓存头配置,可在保证缓存效率的同时,实现真正基于内容变化的精准更新检测,避免不必要的资源重新加载。