前端静态资源版本管理:文件指纹与增量更新
为什么版本管理是缓存的前提
《浏览器缓存完全指南》 里反复提到一句话——“文件名带 hash 的资源可以放心设一年强缓存”。
但问题来了:hash 怎么生成?怎么保证改一个文件只更新一个文件?要不要给每个图片也打 hash?CDN 缓存怎么刷新?
这些就是前端静态资源版本管理要回答的问题。
Content Hash:版本管理的基石
什么是 content hash
content hash 是根据文件内容生成的唯一指纹。内容不变,hash 不变;内容变了,hash 必变。
// 构建产物示例
dist/
├── index.html # 不带 hash
├── assets/
│ ├── main.a3b8f9e7.js # content hash
│ ├── vendor.c4d5e6f8.js # content hash
│ └── style.7c2d1e3f.css # content hash
这样你就可以放心地在 Nginx 上配一年强缓存了——具体怎么配可以看 《Nginx 静态资源缓存配置实战》。
Webpack 配置
// webpack.config.js
module.exports = {
output: {
filename: 'assets/[name].[contenthash:8].js',
chunkFilename: 'assets/[name].[contenthash:8].chunk.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new MiniCssExtractPlugin({
filename: 'assets/[name].[contenthash:8].css'
})
]
};
Vite 配置
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name]-[hash:8].js',
chunkFileNames: 'assets/[name]-[hash:8].js',
assetFileNames: 'assets/[name]-[hash:8][extname]'
}
}
}
});
代码拆分:不要把所有东西打成一个文件
之前遇到过一个项目,把所有 JS 打进一个 bundle.js,改一行文案整个文件 hash 都变了。用户每次更新都得下载全部代码。
正确的做法是按需拆分:
1. 第三方库单独打包(vendor)
// Webpack
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
Vite 默认就帮你做了这个,不用额外配。
2. 路由级别的懒加载
// React
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
// Vue
const Dashboard = () => import('./pages/Dashboard.vue');
每个路由页面打成独立 chunk,用户只访问首页就只下载首页的代码。
3. 业务公共模块
splitChunks: {
cacheGroups: {
common: {
minChunks: 2,
minSize: 20000,
name: 'common',
chunks: 'all'
}
}
}
被至少两个页面引用的模块提取到 common.js,避免重复下载。
图片资源也要管理
很多人只给 JS/CSS 加 hash,图片直接扔静态目录。但你想想:
- 一张
logo.png换了新图,文件名没变,用户浏览器里还是旧图 - CDN 缓存不刷新,即使 Nginx 配了过期时间也无济于事
解决方式有两种:
方式一:构建工具自动加 hash
// Webpack
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
generator: {
filename: 'assets/[name]-[hash:8][ext]'
}
}
方式二:文件名直接版本号
对于 Logo、ICON 这种极少变动的资源:
logo-v2.png
favicon-v3.ico
manifest:版本映射清单
构建工具可以生成一份 manifest 文件,记录原始路径和带 hash 的路径之间的关系:
{
"main.js": "assets/main.a3b8f9e7.js",
"style.css": "assets/style.7c2d1e3f.css",
"logo.png": "assets/logo-e4f5a6b7.png"
}
服务端渲染时读这个 manifest,自动注入正确的资源路径。
const manifest = require('./dist/manifest.json');
function assetPath(name) {
return manifest[name] || name;
}
纯静态站点可以用构建插件把资源路径替换掉,比如 Webpack 的 HtmlWebpackPlugin 或 Vite 自身的资源处理。
版本回滚:hash 的好处
有时候上线后发现问题需要回滚,如果你的资源文件名不带 hash,回滚意味着要重新部署旧代码,还要清 CDN 缓存。整个过程可能要好几分钟。
但如果你用了 content hash:
- 旧的 hash 文件还留在服务器上
- 回滚只需把
index.html恢复成旧版本 - 旧版 HTML 会自动引用旧的 hash 文件
- CDN 缓存天然命中(路径没变)
回滚就在一瞬间。
CDN 缓存刷新策略
就算你配好了 hash 和强缓存,上线后 CDN 上可能还存着老版本的资源。两种做法:
策略一:hash 文件不主动刷新
因为 hash 变了就是新路径,CDN 不会有缓存冲突。你只需要刷新 index.html 的 CDN 缓存。
# 以阿里云 CDN 为例
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://example.com/index.html" \
--ObjectType File
策略二:批量清理 CDN 目录
# 清理整个 /assets/ 目录下的缓存
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://example.com/assets/" \
--ObjectType Directory
一个完整的工作流
本地开发 → 构建 → 生成带 hash 的资源
↓
上传到 CDN / 服务器
↓
部署 index.html(更新资源引用)
↓
刷新 index.html 的 CDN 缓存
↓
用户请求 → 新 HTML → 引用新 hash 文件 → 缓存命中 ✅
总结
| 策略 | 作用 | 最佳配合 |
|---|---|---|
| Content Hash | 内容不变 hash 不变,实现长期缓存 | Nginx immutable 一年强缓存 |
| 代码拆分 | 按需加载,增量更新 | 路由懒加载 + vendor 分包 |
| Manifest 映射 | 服务端/构建工具自动匹配路径 | SSR 或 SSG 场景 |
| CDN 缓存刷新 | 更新后让用户立即获取新资源 | 只刷新 HTML,不刷 hash 资源 |
做好资源版本管理,你的缓存策略才算真正落地。原理层面想深入了解,推荐看 《浏览器缓存完全指南》;服务器配置层面,《Nginx 静态资源缓存配置实战》 里有完整的配置模板。