NGINX 缓存
情况背景
你是否也经历过这样的场景:刚刚加班加点修复了一个线上紧急 Bug,满怀信心地通知用户刷新页面,得到的反馈却是“问题依旧”。你无奈地回复:“请您清一下浏览器缓存或者开无痕模式试试”,内心却在咆哮:都 2025 年了,为什么我的现代化网页应用还需要用户手动清缓存?
标准的缓存失效策略
正常情况下项目中通常采用的“标准”缓存失效策略是采用以下方法:
文件名 Hash 化
HTML 入口文件防缓存
文件名 Hash 化
使用 Vite、Webpack 等构建工具,在打包时为 JS、CSS 等静态资源生成独一无二的 Hash 值文件名。
Vite和Webpack在默认配置下,对构建输出的文件处理方式有所不同,但都可以实现生成带有hash值后缀的文件,不过并不是每次打包都会重新生成全新的hash值,具体情况如下:
Vite
默认情况:
在Vite中,默认会对构建输出的静态资源(js、css等)进行哈希处理,生成一个基于文件内容的哈希值,并添加到文件名后缀中。但是,只有当文件内容发生改变时,才会重新生成新的hash值。
比如你开发一个Vue3项目,有一个组件的逻辑代码没有修改,Vite在打包时,该组件对应的js文件的hash值就不会改变。这是因为Vite基于ESBuild进行打包,它对文件的变化检测很高效,只有文件内容有实质性变动,才会重新计算并生成新的hash。
缓存 busting:
这种机制可以很好地利用浏览器缓存。当你部署新版本应用时,未修改的文件由于hash值不变,浏览器会直接从缓存中读取,提升加载性能;而修改过的文件hash值改变,浏览器会重新请求新文件,保证用户获取到最新内容。
配置自定义:
你还可以通过插件或者配置项来自定义哈希行为。例如使用
rollup-plugin-hash等插件,进一步调整哈希的生成规则,或者控制哪些文件需要生成哈希、哈希值的长度等 。
Webpack
默认情况:
Webpack本身没有默认直接为所有输出文件生成基于内容的hash。但在配置
output.filename和output.chunkFilename时,通过使用[contenthash]占位符,可以实现根据文件内容生成hash值。当文件内容没有变化时,hash值不会重新生成。比如在一个React项目中,你只是修改了某个组件的注释,并没有改变实际的代码逻辑,那么该组件打包后的js文件的
contenthash值不会改变 。chunkhash和hash:
Webpack中还有
[hash]占位符,它是根据整个构建过程的上下文计算出来的哈希值,只要构建过程中有任何变化(包括不同文件的增删改),所有使用[hash]的文件都会生成新的hash值。而[contenthash]更精准,仅根据单个文件内容生成,推荐在生产环境中使用[contenthash]来优化缓存。插件和loader的影响:
此外,Webpack丰富的插件生态系统也会影响哈希生成。比如
terser-webpack-plugin在压缩代码时,如果配置不当,可能会导致即使文件内容逻辑未变,但因为压缩后的格式微调等原因,生成新的contenthash。同时,一些loader在处理文件时,也可能间接影响文件内容从而影响哈希值的生成 。
HTML 入口文件防缓存
在 index.html 的 <head> 部分,加上能够禁止缓存的 <meta> 标签
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">这两个方法实现思路是:
index.html不被缓存,每次访问都是最新的。最新的
index.html引用了带新 Hash 的 JS/CSS 文件。浏览器发现文件名变了,自然会去加载新资源。
这样用户看到了最新的页面。
但是在实际项目情况当中,每次重新打包更新前端后,用户刷新还是访问的旧版本的前端内容。
失效原因
主要是因为浏览器缓存优先级问题。
HTTP 响应头(Response Headers)的优先级 > HTML Meta 标签
所以 meta 标签更像是一种“建议”。
而由服务器(如 Nginx)在 HTTP 响应中返回的 Cache-Control、Expires 等头部信息,才是浏览器必须严格遵守的“最高指令”。
如果 HTTP 响应头没有明确指示不缓存,或者指示了可以缓存,那么浏览器就会愉快地忽略 meta 标签的建议,将 index.html 缓存起来。
一般的NGINX配置文件nginx.conf,都没有去管理响应头,都是采用默认的方式。
location / {
try_files $uri $uri/ /index.html;
# 🔴 致命的遗漏:这里没有任何关于 index.html 的缓存控制指令!
}所以用户浏览器访问会变成:
当浏览器请求 https://yoursite.com/ 时,命中了 location / 规则。
Nginx 返回了 index.html 文件,但没有附加任何 Cache-Control 或 Expires 响应头。
浏览器或上游 CDN 看到这个“沉默”的响应,便启用自己的默认缓存策略,将 index.html 缓存了一段时间。
当你部署新版本后,JS 文件名(例如 main.new-hash.js)虽然变了,但用户再次访问时,浏览器直接从缓存中取出了旧的 index.html。
旧的 HTML 文件依然引用着旧的 JS 文件(main.old-hash.js)。
最终,用户看到的还是旧版本,那个 Bug 依然存在。
<meta>标签根本没有任何存在感。
解决方案
那就只要给 NGINX 为不同类型的文件下达明确的缓存策略就能解决。
可用于生产环境的NGINX配置方案:
server {
listen 80;
server_name your.domain.com; # 替换为你的域名
root /usr/share/nginx/html; # 替换为你的项目根目录
# 规则1:HTML 文件 - 永不缓存
# 这是最关键的一步,确保浏览器总是获取最新的入口文件。
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 规则2:带 Hash 的静态资源 - 永久缓存
# 文件名中的 Hash 确保了内容变化时文件名也会变化,所以可以放心地让浏览器永久缓存。
# `immutable` 告诉浏览器这个文件内容永远不会变,连校验请求都无需发送。
location ~* \.[a-f0-9]{8}\.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 规则3:其他静态资源(如图片、字体) - 长期缓存
# 这些文件通常不带 Hash,但也不常变动,可以设置一个较长的缓存时间。
location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public";
}
# 规则4:单页应用(SPA)路由处理
# 这是保证 React/Vue 等路由正常工作的关键。
# 重要的是,它会将所有未匹配到具体文件的请求都交由 index.html 处理。
# 由于我们已为 /index.html 单独设置了不缓存规则,所以这里是安全的。
location / {
try_files $uri $uri/ /index.html;
}
}配置解读
location = /index.html:使用 = 精确匹配 /index.html,并强制其不被任何一方缓存。这是整个策略的核心。location ~* \.[a-f0-9]{8}\.(css|js)$:通过正则表达式匹配所有带 8 位 Hash 的 JS 和 CSS 文件,并设置长达一年的缓存(1y)和 immutable 属性,实现最佳性能。location /:作为最后的 fallback,处理 SPA 的前端路由,将所有页面导航都指向不缓存的 index.html。
将这份配置应用到你的 nginx.development.conf, nginx.testing.conf, nginx.production.conf, 和 nginx.preview.conf 文件中(根据不同环境微调 expires 时间即可),你将彻底告别缓存带来的烦恼。
文章参考: