---
URL: "/zh-CN/blog/2025-11/nginx-cache-conf.html"
LLMS_URL: "/zh-CN/blog/2025-11/nginx-cache-conf.md"
lastUpdated: true
commentabled: true
recommended: true
title: "什么？5202年了发版后还要手动清浏览器缓存？"
description: "什么？5202年了发版后还要手动清浏览器缓存？"
date:

pageClass: "blog-page-class"
cover: "/covers/Nginx.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: 什么？5202年了发版后还要手动清浏览器缓存？
description: 什么？5202年了发版后还要手动清浏览器缓存？
date: 2025-11-03 09:20:00 
pageClass: blog-page-class
cover: /covers/Nginx.svg
---

## 破解浏览器缓存之谜：为何你的 Webpack/Vite/Rsbuild 应用总是无法自动更新？ ##

你是否也经历过这样的场景：刚刚加班加点修复了一个线上紧急 Bug，满怀信心地通知用户刷新页面，得到的反馈却是“问题依旧”。你无奈地回复：“请您清一下浏览器缓存或者开无痕模式试试”，内心却在咆哮：都 `5202` 年了，为什么我的现代化 Webpack 应用还需要用户手动清缓存？

如果这个场景让你感同身受，那么这篇文章就是为你准备的。我们将深入一个前端开发中最常见也最令人头疼的问题——浏览器缓存，并从根源上剖析为何我们看似“完美”的缓存策略会失效，最终提供一个一劳永逸的 `Nginx` 配置方案，让你的应用实现真正的无感知、自动化更新。

## 一、 “完美”的伪装：我们以为万无一失的缓存策略 ##

在深入问题之前，让我们先审视一下项目中通常采用的“标准”缓存失效策略。这套组合拳看起来无懈可击：

### 第一招：文件名 Hash 化 ###

我们使用 Webpack/Vite 等构建工具，在打包时为 JS、CSS 等静态资源生成独一无二的 Hash 值文件名。

```txt
main.58d91471.js
runtime.0bb7b510.js
vendor.ant-design.59d332b0.js
```

理论上，只要文件内容发生变化，Hash 就会改变，浏览器就会请求新的文件。

### 第二招：HTML 入口文件防缓存 ###

我们在 `index.html` 的 `<head>` 部分，虔诚地加入了所有能想到的禁止缓存的 meta 标签。

```html
<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 的配置文件。不看不知道，一看吓一跳（以一个典型的测试环境配置为例）：

```nginx
# 问题1：这条规则很棒，但只对图片、JS、CSS等资源生效，HTML文件被完美错过！
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
    expires 1d;
    add_header Cache-Control "public, max-age=86400";
}

# 问题2：处理所有路由请求的核心区域，但......
location / {
    try_files $uri $uri/ /index.html;
    # 🔴 致命的遗漏：这里没有任何关于 index.html 的缓存控制指令！
}
```

问题瞬间清晰了：

1. 当浏览器请求 `https://yoursite.com/` 时，命中了 `location /` 规则。
2. Nginx 返回了 `index.html` 文件，但没有附加任何 `Cache-Control` 或 `Expires` 响应头。
3. 浏览器或上游 CDN 看到这个“沉默”的响应，便启用自己的默认缓存策略，将 `index.html` 缓存了一段时间。
4. 当你部署新版本后，JS 文件名（例如 `main.new-hash.js`）虽然变了，但用户再次访问时，浏览器直接从缓存中取出了旧的 `index.html`。
5. 旧的 HTML 文件依然引用着旧的 JS 文件（`main.old-hash.js`）。
6. 最终，用户看到的还是旧版本，那个让你抓狂的 Bug 依然存在。

整个流程形成了一个完美的闭环，`meta` 标签在其中毫无存在感。

## 三、 终极解决方案：精细化 Nginx 缓存策略 ##

既然找到了根源，解决方案就变得非常直接：我们必须通过 Nginx，为不同类型的文件下达明确、精细的缓存“指令”。

以下是一份经过实战检验的、可以安全用于生产环境的 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.staging.conf` 文件中（根据不同环境微调 `expires` 时间即可），你将彻底告别缓存带来的烦恼。

## 四、 别忘了 CDN ##

如果你的应用部署在 CDN 之后，请确保 CDN 的缓存策略与你的 Nginx 配置保持一致。通常需要在 CDN 控制台设置规则，使其“*遵守源站（Origin）的 Cache-Control 头*”。


|  文件类型   |  建议 TTL  |  是否遵守源站  |
| :-----------: | :-----------: | :-----------: |
| `*.html` | 0 秒 | 是 |
| `*.[hash].js` | 31536000 秒 (1年) | 是  |
| `*.[hash].css` | 31536000 秒 (1年) | 是 |
| 图片/字体 | 2592000 秒 (30天) | 是 |

## 总结 ##

Web 应用的缓存问题，看似玄学，实则逻辑清晰。其根本在于 *HTTP 响应头是控制缓存的唯一权威*。通过在 Nginx 层实施精细化的缓存策略——*让入口 HTML 永不缓存，让带 Hash 的静态资源永久缓存*——我们不仅能从根源上解决用户无法看到更新的问题，还能最大化地利用缓存来提升应用性能。

现在，是时候去检查一下你的 Nginx 配置了。告别“请清缓存”，让每一次发布都如丝般顺滑。

## 多版本支持 ##

在你的场景下，`history/` 目录是上一个版本，`main/` 目录是当前版本。你希望通过 Nginx 反向代理，并利用 `try_files` 机制，使页面刷新时仍能正常访问（避免 404），同时支持 SPA 前端路由，并且能够切换不同版本的资源。

### 一、Nginx 解决方案 ###

你可以使用 `try_files` 结合 `main/` 和 `history/` 目录，实现：

- 默认加载 `main/` 版本
- 如果 `main/` 目录找不到资源，就回退到 `history/`
- 支持前端 SPA 刷新

#### Nginx 配置 ####

```nginx
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        root /var/www/html;
        index index.html;
        try_files $uri $uri/ /main/index.html;
    }

    # 代理 main 版本
    location /main/ {
        root /var/www/html;
        index index.html;
        try_files $uri $uri/ /main/index.html;
    }

    # 代理 history 版本（如果 main 目录找不到文件，则回退到 history 版本）
    location /history/ {
        root /var/www/html;
        index index.html;
        try_files $uri $uri/ /history/index.html;
    }

    # 处理前端路由刷新 404 问题
    location /app/ {
        root /var/www/html;
        index index.html;
        try_files $uri /main/index.html;
    }

    error_page 404 /main/index.html;
}
```

### 二、关键点解析 ###

`try_files $uri $uri/ /main/index.html;`

先尝试访问请求的静态文件 (`$uri`)，如果找不到，再尝试访问目录 (`$uri/`)，最后回退到 `main/index.html` 处理 SPA 路由。

`支持 main/ 和 history/ 目录`

- `/main/` 目录是当前版本，默认使用它。
- `/history/` 目录是上一个版本，如果 `main/` 目录中找不到资源，就会尝试从 `history/` 目录加载。

`error_page 404 /main/index.html;`

避免用户直接刷新时 404，Nginx 会回退到 `main/index.html` 让前端框架处理路由。

### 三、前端如何切换版本？ ###

#### 通过 URL 参数控制 ####

你可以在前端代码中检测 URL 参数，如 `?version=history`，然后动态切换资源：

```javascript
const version = new URLSearchParams(window.location.search).get("version") || "main";
document.write(`<script src="/${version}/app.js"></script>`);
```

#### Nginx 变量控制 ####

如果你希望动态切换版本，可以使用 Nginx 变量：

```nginx
set $version main; # 默认版本
if ($arg_version = history) {
    set $version history;
}
location / {
    try_files $uri /$version/index.html;
}
```

这样，你可以通过 `yourdomain.com?version=history` 访问历史版本。

### 三、总结 ###

其中 `try_files` 机制适用于 SPA 应用的版本切换，切换新旧版本简单，不影响前端代码，需要正确配置 Nginx。

这个方案可以完美解决前端发版导致的页面刷新问题，同时支持新旧版本共存！
