网络与安全
HTTP 缓存:讲清 Cache-Control
no-cache 并不是不缓存。搞懂强缓存和协商缓存,再记住“HTML 要验证,带 hash 的资源缓存一年”这条策略就够了。
两层缓存
浏览器拿到一个请求时,会依次判断:
- 强缓存:本地有副本且还在有效期内,直接使用,根本不发请求。
- 协商缓存:副本过期了,带上标识问服务器“内容变了吗”。没变就返回
304,没有响应体,继续用本地副本;变了就返回200和新内容。
# 第一次响应
HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "abc123"
# 过期后再次请求
GET /app.css HTTP/1.1
If-None-Match: "abc123"
# 内容没变
HTTP/1.1 304 Not ModifiedETag 配 If-None-Match 是基于内容标识的验证,Last-Modified 配 If-Modified-Since 是基于修改时间的验证。两者都有时,优先看 ETag。
Cache-Control 指令
| 指令 | 含义 |
|---|---|
max-age=N | 响应在 N 秒内是新鲜的,期间可以直接使用 |
no-cache | 可以存,但每次使用前都必须向服务器验证 |
no-store | 完全不存,用于敏感数据 |
public | CDN 等共享缓存也可以存 |
private | 只有用户自己的浏览器可以存 |
s-maxage=N | 只对共享缓存生效,优先于 max-age |
immutable | 有效期内内容不会变,部分浏览器在刷新时也不再验证 |
stale-while-revalidate=N | 过期后 N 秒内可以先用旧内容,同时在后台更新 |
最容易混淆的就是 no-cache:它的意思不是“不缓存”,而是“用之前先问一下”。真正不缓存的是 no-store。
另一个常见误解:不写 Cache-Control 不等于不缓存。如果响应带有 Last-Modified,浏览器可以自己估算一个有效期,这叫启发式缓存,常常是“线上改了用户却看不到”的原因。
一条够用的策略
前端项目构建后,通常有带内容 hash 的静态资源(比如 app.3f2a1c.js)和入口 HTML。内容一变,文件名就变,所以可以这样配:
| 资源 | 配置 | 原因 |
|---|---|---|
| HTML 入口 | no-cache | 每次验证,发布后用户马上能拿到新版本 |
| 带 hash 的 JS、CSS、图片 | public, max-age=31536000, immutable | 文件名变了才是新内容,可以放心缓存一年 |
| 不带 hash 的资源 | 短 max-age 配 ETag | 无法靠文件名判断更新 |
| 个人数据接口 | private, no-cache 或 no-store | 不能让共享缓存存下用户数据 |
对应的 nginx 配置:
location = /index.html {
add_header Cache-Control "no-cache";
}
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}几个实际会遇到的问题
- **nginx 的
add_header不会叠加继承。** 如果在location里写了add_header,外层server里配置的响应头(比如安全相关的头)在这个location中就全部失效了,需要重新写一遍。 - 刷新的行为不同。 普通刷新通常会重新验证页面本身;强制刷新(Ctrl/Cmd + Shift + R)会跳过缓存重新下载。调试时也可以在 DevTools 的 Network 面板勾选 Disable cache。
- **别忘了
Vary。** 同一个地址可能根据请求头返回不同内容,比如压缩格式。Vary: Accept-Encoding告诉缓存要把它们分开存,否则可能把压缩过的内容发给不支持的客户端。
怎么检查
打开 DevTools 的 Network 面板:
- Size 列显示
disk cache或memory cache,说明命中了强缓存,没有发请求。 - Status 为
304,说明走了协商缓存,发了请求但没有下载内容。 - 点开请求,在 Response Headers 里确认
Cache-Control是否符合预期。