package handler import ( "io/fs" "net/http" "path" "strings" ) // indexHandler 服务嵌入式前端首页 web/static/index.html。 func indexHandler(fsys fs.FS) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { b, err := fs.ReadFile(fsys, "index.html") if err != nil { http.Error(w, "前端页面尚未构建:请在 frontend 目录执行 pnpm build", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(b) } } // mimeFallback 构建产物常见静态资源的 Content-Type 兜底表 // (mime.TypeByExtension 在部分 Windows 环境依赖系统注册表,结果不稳定)。 var mimeFallback = map[string]string{ ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".webp": "image/webp", } // assetHandler 服务 vite 构建产物 /assets/。 // 前端使用 hash 路由,静态托管只需覆盖根路径与扁平的 assets 目录。 func assetHandler(fsys fs.FS) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.EscapedPath(), "/assets/") if name == "" || strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") { http.NotFound(w, r) return } if _, err := fs.Stat(fsys, "assets/"+name); err != nil { http.NotFound(w, r) return } ct := mimeFallback[strings.ToLower(path.Ext(name))] if ct == "" { ct = "application/octet-stream" } w.Header().Set("Content-Type", ct) w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") // vite 产物带内容哈希 b, err := fs.ReadFile(fsys, "assets/"+name) if err != nil { return } _, _ = w.Write(b) } }