perf(static): 带 hash 的前端资源缓存一年,index.html 不缓存

线上实测 /assets/index-u1PUen4p.js 返回的是 express.static 的默认
Cache-Control: public, max-age=0 —— 每开一次页面就要为每个 JS/CSS 跑一趟
回源校验。而这些文件名里本来就带内容 hash,内容一变文件名就变。

分两档:
  assets/*    max-age=31536000, immutable(连 304 都不用问)
  index.html  no-cache

index.html 必须不缓存:它文件名固定、里面引用的才是带 hash 的资源名。
缓存住的话发了新版用户还拿着旧 index.html、指向旧资源,等于白更新。

SPA fallback 那条单独加了一遍 no-cache —— 它走 sendFile,不经过
express.static 的 setHeaders,/orders 这类深链拿到的同样是 index.html。

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Drsakura
2026-10-22 16:17:44 +08:00
co-authored by Claude Opus 5
parent 3380dbc9d7
commit dc607e4ea3
4 changed files with 28 additions and 5 deletions
+25 -2
View File
@@ -126,8 +126,27 @@ function badRequest(message) {
且表头必须正好在第一行。现在换成「关键词/正则 + 权重打分 + 表头行探测 + 数值列体检」,
并支持 xlsx/xls —— 换行业只需改配置,不用改代码。 */
// ---------------------------------------------------------------------------
// 静态文件服务
app.use(express.static(path.join(__dirname, '../frontend/dist')));
//
// 缓存分两档,因为这两类文件的更新方式完全不同:
//
// assets/* 文件名里带内容 hash(index-u1PUen4p.js),内容一变文件名就变,
// 所以可以放心缓存一年;immutable 让浏览器连 304 都不用问。
// express.static 默认是 max-age=0,等于每开一次页面就为每个
// JS/CSS 跑一趟回源校验 —— 挂在 CDN 后面尤其浪费。
//
// index.html 文件名固定,里面引用的才是带 hash 的资源名。它**必须不缓存**,
// 否则发了新版用户还拿着旧 index.html、指向旧资源,等于白更新。
// ---------------------------------------------------------------------------
const DIST_DIR = path.join(__dirname, '../frontend/dist');
app.use(express.static(DIST_DIR, {
setHeaders(res, filePath) {
if (filePath.endsWith('index.html')) res.setHeader('Cache-Control', 'no-cache');
else res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}));
// API 路由
app.get('/api/health', (req, res) => {
@@ -1801,8 +1820,12 @@ app.use((error, req, res, next) => {
});
// SPA fallback - 所有未匹配的路由返回 index.html
// 这里走的是 sendFile,不经过上面 express.static 的 setHeaders,
// 所以 no-cache 得自己加一遍:/orders 这类深链拿到的也是 index.html,
// 被缓存住的话发新版后用户照样打不到新资源。
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
res.setHeader('Cache-Control', 'no-cache');
res.sendFile(path.join(DIST_DIR, 'index.html'));
});
// 启动服务器