这是困扰我很久的一个问题,每次更新 Ant Design Pro 开发的管理后台前端版本后,浏览器总是会缓存旧的文件,导致用户看到的仍然是旧版本的界面。
或者某个子功能更新,更新后,如果用户没有刷新页面,直接访问这个功能,直接白屏报错。只有当用户手动刷新页面后才能看到更新后的界面。这个体验非常的不好。
之前没怎么当回事,要么是因为后台更新频率不高,要么是后台用的人少,现在遇到了几个系统,用户量比较大,更新频率也比较高,这个问题就变得非常突出,必须要解决。
解决思路
问了一下 DeepSeek, 他说这个问题的根源在于浏览器缓存了旧的 index.html 文件,虽然其他静态资源(js、css)文件名里都带了哈希(例如,app.123456.js),理论上应该是不会被缓存的,但 index.html 没有带哈希,所以浏览器会一直缓存旧的 index.html 文件,导致用户看到的界面一直是旧版本的。
处理前的请求返回
HTTP/1.1 304 Not Modified
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 31 Mar 2026 09:06:17 GMT
Last-Modified: Mon, 02 Feb 2026 08:35:01 GMT
Connection: keep-alive
ETag: "698061b5-1881"
新的 Nginx 配置
server {
listen 80;
server_name _;
client_max_body_size 8M;
# 前端静态文件根目录(提到 server 层)
root /home/some_user/some_project/frontend/dist;
index index.html index.htm;
# API 请求转发到后端 golang 服务
location /api/ {
proxy_set_header X-Forward-For $remote_addr;
proxy_set_header X-real-ip $remote_addr;
proxy_pass http://127.0.0.1:9999/api/;
}
# 所有 HTML 文件强制不缓存。存在子功能对应目录也包含 index.html 的情况,所以不能只针对根目录的 index.html 进行处理。
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
try_files $uri =404;
}
# SPA 路由处理:其他所有请求返回 index.html
location / {
try_files $uri $uri/ /index.html =404;
}
}
检查 Nginx 配置,并 reload Nginx:
sudo nginx -t
sudo nginx -s reload
处理后的请求返回
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 31 Mar 2026 08:33:30 GMT
Content-Type: text/html
Last-Modified: Mon, 02 Feb 2026 08:35:01 GMT
Transfer-Encoding: chunked
Connection: keep-alive
ETag: W/"698061b5-1881"
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
Content-Encoding: gzip
项目目录结构参考
我发现 Ant Design Pro 的前端项目目录结构中,index.html 文件是放在 dist 目录下的,同时每个子功能也有对应的子目录,里面也包含一个 index.html 文件。
而 Vue Element 则只有一个根目录的 index.html 文件,没有子功能目录下的 index.html 文件,其他都在 js 目录下。
修改后文件的 etag 是不同的,为啥还会被缓存呢?
虽然 index.html 文件的缓存问题解决了,但是我不太理解,为何修改前的返回里,也有文件的 etag 和修改时间等信息,且 etag 也不一样了,但浏览器还是会缓存旧的 index.html 文件呢?
原因是没有设置 Cache-Control: no-cache 等响应头,浏览器依然可能使用旧的缓存,而不会主动向服务器验证。
当浏览器缓存了一个资源(比如 index.html),再次请求时,是否发送验证请求取决于缓存响应的 Cache-Control 和 Expires 头。
如果响应头中带有 Cache-Control: no-cache(或 must-revalidate)。浏览器每次都会向服务器发送验证请求(带上 If-None-Match 或 If-Modified-Since),服务器根据 ETag 或 Last-Modified 决定返回 200(新内容)还是 304(未修改,使用缓存)。
如果没有 Cache-Control 或 Expires 明确指示。浏览器会使用启发式缓存(Heuristic Caching)。例如,根据 Last-Modified 时间,浏览器可能认为资源在 (当前时间 - 上次修改时间) * 10% 的时间内是新鲜的,期间根本不会发送请求,直接使用本地缓存。这就会导致即使服务器上的文件已更新(ETag 变了),浏览器也不去验证,用户看到的还是旧页面。
而用户刷新页面(F5 或 Ctrl+R):浏览器通常会忽略启发式缓存,强制发送验证请求(带上 If-None-Match),这时 ETag 不同就会返回新内容。
ETag 的作用
ETag 本身并不控制是否验证,它只用于验证时的比较。如果没有验证,ETag 再变也无用。因此,对于 index.html 这类必须实时更新的文件,必须设置 Cache-Control: no-cache(或其他强制验证的指令),让浏览器每次都必须向服务器询问,确保及时拿到最新版本。
Nginx 默认的 ETag 格式:
ETag = {文件最后修改时间的十六进制} - {文件内容长度的十六进制}
详情参考:Golang Gin Static 缓存大坑:embed 文件无法被 CDN 和浏览器缓存
关于作者 🌱
我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式