// Package webui 嵌入前端构建产物(go:embed)。 // // 构建方式:先构建 web/(Vite 产物输出到 web/dist),再 go build。 // 未构建前端时(骨架阶段 / go test / 纯后端开发)由 NewHandler 提供 // SPA fallback 占位,保证服务可启动、/healthz 可用。 package webui import ( "bytes" "embed" "io/fs" "net/http" "strings" "time" ) //go:embed all:dist var dist embed.FS // NewHandler 返回静态资源 handler。前端产物存在时提供 SPA fallback // (非 API 路径回退 index.html,交给前端路由);否则返回占位页。 func NewHandler() http.Handler { sub, err := fs.Sub(dist, "dist") if err != nil { return placeholderHandler("前端产物未嵌入(先执行 make web-build)") } indexBytes, err := fs.ReadFile(sub, "index.html") if err != nil { return placeholderHandler("前端产物未嵌入(先执行 make web-build)") } fileServer := http.FileServer(http.FS(sub)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/") if p == "" { p = "index.html" } // 存在的静态资源直接返回(含版本化 assets/ 与 favicon) if f, err := sub.Open(p); err == nil { f.Close() fileServer.ServeHTTP(w, r) return } // SPA fallback:其余路径回退 index.html w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(indexBytes)) }) } func placeholderHandler(msg string) http.Handler { body := "ws_usernode" + "

ws_usernode

" + msg + "

" + "

健康检查:/healthz

" return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(body)) }) }