package static import ( "net/http" "regexp" "strings" ) // hashedAsset 匹配 Vite 产出的内容哈希文件名,例如 index-DUb2s9Ly.js、CalendarView-BQ1MXzlA.css。 // // 判据是「主名-哈希.扩展名」,哈希为 8 位以上字母数字(含 - 与 _, // Vite 用 URL-safe base64 变体)。**故意不宽松匹配**:只有真正带内容哈希的 // 文件才敢配 immutable,配错了会让更新永远拿不到。 var hashedAsset = regexp.MustCompile(`-[A-Za-z0-9_-]{8,}\.[a-z0-9]+$`) // CacheControl 给静态前端加缓存策略。 // // # 为什么必须区分两类 // // 「换了新前端但用户看到的还是旧界面」几乎总是因为入口页被缓存:Vite 给每个 // 资源按内容加哈希,新构建会生成**新的文件名**,但 `index.html` 里引用的是哪个 // 文件名,取决于用户拿到的是哪一代 HTML。给 HTML 加了长缓存,等于把整站钉死在 // 那一代资源上,而且**没有任何报错** —— 只有人肉硬刷新才能发现。 // // 所以: // - **入口页每次回源校验**(no-cache,不是 no-store:允许落盘但要先确认)。 // 它只有 ~2KB,回源代价可忽略,而它决定了用户拿到哪一代资源。 // - **带内容哈希的资源永久缓存**(immutable)。内容变了文件名就变, // 因此不存在「缓存了旧内容」的问题,连回源都不需要。 // - **不带哈希的资源不缓存**。`STATIC_DIR` 指向开发目录时文件名可能没有哈希, // 这时给 immutable 会让改动永远不生效 —— 比缓存旧资源更难查。 func CacheControl(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if IsImmutableAsset(r.URL.Path) { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "no-cache") } h.ServeHTTP(w, r) }) } // IsImmutableAsset 判断一个静态路径能否安全地永久缓存。 func IsImmutableAsset(path string) bool { if !strings.HasPrefix(path, "/assets/") { return false } return hashedAsset.MatchString(path) } // SetIndexCacheControl 给入口页写缓存策略。 // // 与 CacheControl 分开是因为入口页走的是独立的 handler(不在 /assets/ 前缀下), // 而它的策略是固定的:必须每次回源校验。 func SetIndexCacheControl(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache") }