










程序启动后在一个原生 WebView 窗口中加载 static 目录下的前端页面,前端通过 Ajax 请求本程序内置的 RESTful API。所有静态资源在编译时通过 //go:embed 内嵌到可执行文件,发布时只需一个二进制。
// SIMLive — 自包含桌面应用:内嵌 Web 界面 + Go RESTful API 服务。 // // 本程序启动后在一个原生 WebView 窗口中加载 static 目录下的前端页面, // 前端通过 Ajax 请求本程序内置的 RESTful API。所有静态资源在编译时 // 通过 //go:embed 内嵌到可执行文件,发布时只需一个二进制。 package main import ( "SIMLive/frameless" "embed" "encoding/json" "io/fs" "log" "net" "net/http" "time" "github.com/crgimenes/glaze" ) //go:embed all:static var staticFS embed.FS const addr = "127.0.0.1:8080" const width = 480 const height = 320 const title = "SIMLive 桌面控制台" const dev = false // 开发模式:不带窗口,仅把静态资源 + API 挂到 127.0.0.1:8080, 并开启 CORS,供 HBuilder 等工具单独预览 / 调试前端页面。 const titleBar = false // 带标题栏窗口(最小化,最大化,关闭) func main() { mux := http.NewServeMux() mux.Handle("/", http.FileServer(http.FS(mustSub(staticFS, "static")))) // 静态文件托管 mux.HandleFunc("/api/hello", handleHello) // RESTful API if dev { log.Printf("开发模式:请在 HBuilder 中预览 static 目录,或直接访问 http://%s/index.html", addr) if err := http.ListenAndServe(addr, withCORS(mux)); err != nil { log.Fatalf("开发服务器启动失败: %v", err) } return } if titleBar { // 带标题栏窗口(最小化,最大化,关闭) if err := glaze.AppWindow(glaze.AppOptions{ Title: title, Width: width, Height: height, Transport: glaze.AppTransportTCP, Addr: addr, Handler: mux, }); err != nil { log.Fatalf("窗口运行失败: %v", err) } } else { // 以无边框窗体呈现 if err := runWindow(mux, addr); err != nil { log.Fatalf("窗口运行失败: %v", err) } } } // runWindow 是 AppWindow 的等价实现, 整个标题栏(即去除最小化/最大化/关闭按钮)。 func runWindow(handler http.Handler, addr string) error { ln, err := net.Listen("tcp", addr) if err != nil { return err } defer ln.Close() srv := &http.Server{Handler: handler, ReadHeaderTimeout: 10 * time.Second} defer srv.Close() go func() { _ = srv.Serve(ln) }() // glaze.New 会将当前 goroutine 绑定到 UI 线程,后续窗口操作都须在它上面进行。 w, err := glaze.New(false) if err != nil { return err } w.SetTitle(title) if err := frameless.MakeFrameless(w.Window()); err != nil { return err } w.SetSize(width, height, glaze.HintNone) w.Navigate("http://" + ln.Addr().String()) w.Run() w.Destroy() return nil } // withCORS 仅在开发模式开启,允许 HBuilder 预览页面跨域调用本服务。 func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // mustSub 从内嵌 FS 中取子目录,失败即终止。 func mustSub(fsys embed.FS, dir string) fs.FS { sub, err := fs.Sub(fsys, dir) if err != nil { log.Fatalf("无法读取内嵌目录 %q: %v", dir, err) } return sub } // handleHello 返回示例 JSON 数据。新增业务接口时,按相同模式在此注册即可。 func handleHello(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) http.Error(w, "仅支持 GET", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") if err := json.NewEncoder(w).Encode(map[string]string{ "message": "Hello from Go API!", }); err != nil { log.Printf("写入响应失败: %v", err) } }
程序自启后,加载webview

调试模式:Hbuild加载html,访问

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。