首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >官网一发版用户就看到旧页面、样式还错乱:静态资源 contenthash、强缓存与缓存破坏的发版方案

官网一发版用户就看到旧页面、样式还错乱:静态资源 contenthash、强缓存与缓存破坏的发版方案

原创
作者头像
用户5598620
发布于 2026-09-24 14:00:50
发布于 2026-09-24 14:00:50
880
举报

官网每次发版后,最常见的几类客诉是:"我看到的还是旧页面""排版和图片全乱了""按钮点了没反应"。

这些问题大多不是新代码没传上去,而是缓存配错了:入口 HTML 和它引用的 JS、CSS 用了同一套缓存策略。本文给一套可以直接落地的发版缓存方案——HTML 每次校验、静态资源带内容指纹永久强缓存、新旧版本并存,附构建配置、nginx 配置和发版检查清单。

一、旧页面和样式错乱,到底是怎么来的

先补两条缓存基础。强缓存通过 Cache-Control 的 max-age 控制,命中时浏览器不向服务器发请求,直接用本地副本;协商缓存通过 ETag 或 Last-Modified 校验,服务器返回 304 时复用本地副本。

发版事故基本是下面三种:

  • index.html 被强缓存:用户手里是旧 HTML,里面引用旧版本的 JS、CSS 文件名;新资源已经上线,但旧 HTML 还指向旧文件,于是长期停在旧版。
  • 资源文件名固定(app.js、style.css)又被强缓存:发版后文件名没变,浏览器直接用旧文件,新逻辑、新样式不生效。
  • 最危险的新旧混用:发版文件传了一半,或 CDN 各节点刷新不一致,出现新 HTML 引用旧资源、旧 HTML 引用新资源,结果样式错乱,JS 因接口结构对不上报 undefined,页面白屏。

关键认知是:HTML 和静态资源应当使用相反的缓存策略。

二、为什么是"HTML 不缓存 + 资源 contenthash 永久强缓存"

index.html 是一份"入口清单",它决定加载哪些 JS、CSS,必须尽量拿到最新,因此设为 no-cache,即每次都向服务器协商校验,而不是长期强缓存。

JS、CSS、图片在内容确定后,文件名里带上内容指纹 contenthash,例如 app.3f9a2c1b.js。内容不变,hash 就不变;内容一改,hash 立即变。这类文件可以放心设一年强缓存并加 immutable 标记。

这样发版后,新 HTML 引用新 hash 文件名,旧用户手里的旧 HTML 引用旧 hash 文件名,两套文件同时存在,谁都不会 404,也不会出现新旧混用。这就是缓存破坏(cache busting):不靠通知浏览器"缓存过期",而是用一个全新的文件名让它自然加载新资源。图片、字体等资源同样按内容 hash 命名后纳入长缓存;只有频繁变化、需要被即时发现的入口 HTML 才保持每次协商校验。

三、构建侧:让产物文件名带上 contenthash

入口 HTML 文件名固定为 index.html、不带 hash;JS、CSS 输出名带 contenthash。webpack 可这样配:

代码语言:javascript
复制
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          require('mini-css-extract-plugin').loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new (require('mini-css-extract-plugin'))({
      filename: '[name].[contenthash:8].css'
    })
  ]
};

Vite 则在 Rollup 输出项里给文件名加 hash:

代码语言:javascript
复制
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash:8].js',
        chunkFileNames: 'assets/[name].[hash:8].js',
        assetFileNames: 'assets/[name].[hash:8].[ext]'
      }
    }
  }
});

建议配合代码分割把第三方库拆成独立 vendor 包。业务代码常变、第三方库很少变,拆出后 vendor 的 hash 长期稳定,用户发版后只需重新下载变化的业务包,缓存利用率更高。

四、服务器与 CDN 侧:两类文件分别配缓存头

nginx 对 index.html 单独设协商缓存,对带 hash 的资源目录设一年强缓存:

代码语言:nginx
复制
server {
  listen 80;
  root /usr/share/nginx/html;

  location = /index.html {
    add_header Cache-Control "no-cache";
  }

  location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri =404;
  }

  location / {
    try_files $uri $uri/ /index.html;
  }
}

CDN 侧回源时遵循这些响应头:HTML 不缓存或只做短缓存,带 hash 的资源长缓存。因为文件名随内容变化,常规发版不需要对 CDN 做全量刷新。资源统一放在 /assets/ 这类目录下,便于用一条 location 规则统一处理,index.html 单独匹配。

五、发版瞬间怎么避免新旧混用白屏

发布要做到原子化。新版本先传到一个全新目录,例如 /release/20260924/,等所有文件传完,再通过软链或重命名一次性切换 index.html 的指向,这一步对文件系统是原子的,不要边传边覆盖线上文件。

旧版本资源要保留一段时间,长度至少覆盖最长在线用户会话加上 HTML 的缓存窗口。手里拿着旧 HTML 的用户在这段时间内仍能加载旧 hash 资源,不会 404。

单页应用还要让路由兜底,任意子路由刷新都回退到 index.html,即上例中的 try_files $uri $uri/ /index.html,否则用户在深层链接处刷新会 404。当前端大改、接口数据结构变化时,后端旧接口保留一个兼容窗口,避免新版页面配上错配接口导致白屏。

六、用户一直开着页面,怎么平滑更新

页面开了一整天的长会话用户不会自动加载新版。可以做轻量版本检测:定时请求一个 version.json 或重新拉取 index.html,与当前构建版本比较,发现新版后提示"页面已更新,点击刷新",由用户决定,而不是强制 location.reload 打断正在填写的表单。

代码语言:javascript
复制
let currentVersion = __BUILD_VERSION__;

async function checkUpdate() {
  try {
    const res = await fetch('/version.json?t=' + Date.now());
    const data = await res.json();
    if (data.version !== currentVersion) {
      saveDraftBeforeReload();
      promptUserToRefresh();
    }
  } catch (e) {
    // 检测失败保持静默,下一轮再试,不影响当前页面
  }
}

setInterval(checkUpdate, 5 * 60 * 1000);

触发刷新提示前先保存草稿或本地状态,用户确认后再重载,把打断成本降到最低。

七、踩坑清单

  • index.html 被强缓存:用户长期停在旧版,HTML 必须设 no-cache。
  • 资源不带 hash 又强缓存:发版不生效,带 contenthash 才能配长缓存。
  • 边传边覆盖旧文件:新旧 HTML、资源混用导致白屏,要原子发布并保留旧版本。
  • 一发版就全量刷新 CDN:没有必要,还可能增加回源压力,带 hash 会自然失效。
  • 立刻删除旧资源:拿着旧 HTML 的用户会 404,保留一个兼容窗口。
  • 检测到新版强制 reload:打断用户操作,应提示并先存草稿。

八、工程落地建议

把缓存策略固化进构建和部署模板,而不是每次发版手动调整:构建产物默认带 contenthash,nginx 和 CDN 规则按 HTML 与 /assets 分开,部署脚本固定走"传新目录、原子切换、延迟清理旧目录"三步。

发版流程中加一步预发验证:在预发环境用新 HTML 完整加载一次新资源,确认无 404、控制台无报错,再切换到正式环境。

九、复盘清单

  • index.html 的响应头是否为 no-cache。
  • JS、CSS、图片是否带 contenthash 并配长缓存。
  • 是否原子切换,旧版本资源是否保留。
  • 子路由刷新是否兜底到 index.html。
  • 变更接口是否保留兼容窗口。
  • 版本更新提示是否不打断操作、是否先保存草稿。

常见问题

官网发版后用户还看到旧页面,是什么原因?

通常是 index.html 被强缓存,或 JS、CSS 文件名没变又被缓存。让 HTML 每次协商校验、给资源文件名加内容 hash,发版后新 HTML 会引用新 hash 资源。

带 hash 的静态资源为什么可以缓存一年?

文件名由内容计算得出,内容不变 hash 就不变,内容一改 hash 立即变;缓存旧 hash 文件不影响新版,新 HTML 会加载新文件名。

发版时要不要立刻删旧资源、刷新 CDN?

都不建议。旧资源保留一个会话兼容窗口,避免拿着旧 HTML 的用户 404;带 hash 的新文件会自然加载,不需要全量刷新 CDN。

发版缓存看似是运维细节,实则直接决定用户看到的是新版还是错乱页面。本文为前端工程实践分享,具体配置以项目实际架构和服务器环境为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、旧页面和样式错乱,到底是怎么来的
  • 二、为什么是"HTML 不缓存 + 资源 contenthash 永久强缓存"
  • 三、构建侧:让产物文件名带上 contenthash
  • 四、服务器与 CDN 侧:两类文件分别配缓存头
  • 五、发版瞬间怎么避免新旧混用白屏
  • 六、用户一直开着页面,怎么平滑更新
  • 七、踩坑清单
  • 八、工程落地建议
  • 九、复盘清单
  • 常见问题
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档