前端靜態資源(JS、CSS、圖片、字體)部署看似簡單,實則暗藏緩存失效、版本衝突、CDN 同步、回滾困難等生產級難題。本文系統梳理從原始到現代的演進路徑,給出生產環境驗證的完整部署策略。
If-Modified-Since/If-None-MatchLast-Modified 秒級精度不足、ETag 需統一算法。Cache-Control: max-age=31536000, immutable (一年)a.css?v=1.0a.css?v=2.0a.css 導致 b.css?v=2.0,b.css 內容未變卻強制客戶端重新下載,浪費帶寬。a.css?v=0abc23 (Webpack 默認 [contenthash:8])Cache-Control: max-age=31536000, immutable
// webpack.prod.js
module.exports = {
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
path: path.resolve(__dirname, 'dist'),
publicPath: 'https://cdn.yoursite.com/', // CDN 域名
clean: true // 每次構建清理舊文件
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: 'index.html',
inject: true // 自動注入帶哈希的 script/link
})
]
};
// vite.config.js
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
output: {
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: (assetInfo) => {
const ext = assetInfo.name.split('.').pop();
if (/\.(png|jpe?g|gif|svg|webp)$/.test(assetInfo.name)) return `img/[name].[hash].${ext}`;
if (/\.(css)$/.test(assetInfo.name)) return `css/[name].[hash].${ext}`;
return `assets/[name].[hash].${ext}`;
}
}
},
manifest: true // 生成 manifest.json 供後端模板渲染使用
},
base: 'https://cdn.yoursite.com/' // CDN 域名
});
| 部署順序 | 時間窗口內用戶體驗 | 後果 |
|---|---|---|
| 先部署頁面,後部署資源 | 新頁面引用新資源 URL,但資源尚未上線 CDN | 404/舊資源 → 頁面樣式錯亂,緩存污染 |
| 先部署資源,後部署頁面 | 舊頁面引用舊資源 URL (命中緩存正常);無緩存用戶請求新資源 URL 但頁面未更新 | 無緩存用戶:舊頁面加載新資源 → JS 報錯/樣式錯亂 |
a.0abc23.css 而非 a.css?v=0abc23。HtmlWebpackPlugin 自動注入manifest.json 後端模板渲染<link rel="stylesheet" href="{% css_hash %}">
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff|woff2|svg)$ {
# 文件名含哈希,可安全設置長期強制緩存
expires 1y;
add_header Cache-Control "public, immutable";
# 關鍵:關閉 etag/last-modified 避免協商緩存干擾
etag off;
if_modified_since off;
# 訪問日誌可選關閉減輕 IO
access_log off;
# 如果使用查詢參數版本,需確保 proxy_cache_key 包含 $args
# proxy_cache_key $scheme$proxy_host$request_uri$args;
}
// vite.config.js + vite-plugin-pwa
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: { name: 'My App', short_name: 'App', theme_color: '#ffffff' },
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,woff2}'],
runtimeCaching: [{
urlPattern: /^https:\/\/api\.yoursite\.com\/.*/,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 3600 } }
}]
}
})
]
});
<head>
<link rel="preload" href="https://cdn.yoursite.com/js/main.a1b2c3d4.js" as="script">
<link rel="preload" href="https://cdn.yoursite.com/css/main.e5f6g7h8.css" as="style">
<link rel="prefetch" href="https://cdn.yoursite.com/js/chunk-detail.i9j0k1l2.js">
</head>
/v1.2.3/, /v1.2.4/ 多版本,頁面指定版本路徑,回滾只需修改頁面版本號。<script src="..." integrity="sha384-..." crossorigin="anonymous"> 防止 CDN 篡改/傳輸損壞。[contenthash:8])Cache-Control: max-age=31536000, immutable)遵循此清單,可實現零緩存沖突、零版本衝突、秒級回滾、極致首屏性能的生產級前端部署體系。