package httpx import ( "io" "io/fs" "net/http" "path" "strings" ) // SPA 提供后台静态页:存在的文件原样返回;无扩展名的前端路由回退 index.html; // 有扩展名但不存在返回 404;访问目录不列清单,同样回退 index.html。 func SPA(fsys fs.FS) http.Handler { files := http.FileServer(http.FS(fsys)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { w.WriteHeader(http.StatusMethodNotAllowed) return } cleaned := path.Clean("/" + r.URL.Path) rel := strings.TrimPrefix(cleaned, "/") if rel == "" { rel = "." } fi, err := fs.Stat(fsys, rel) if err == nil && !fi.IsDir() { files.ServeHTTP(w, r) return } if path.Ext(cleaned) != "" { http.NotFound(w, r) return } serveIndex(w, r, fsys) }) } func serveIndex(w http.ResponseWriter, r *http.Request, fsys fs.FS) { f, err := fsys.Open("index.html") if err != nil { http.NotFound(w, r) return } defer func() { _ = f.Close() }() st, err := f.Stat() if err != nil || st.IsDir() { http.NotFound(w, r) return } w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) if r.Method == http.MethodHead { return } _, _ = io.Copy(w, f) }