惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
博客园 - 叶小钗
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog
S
SegmentFault 最新的问题
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
量子位
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
V2EX
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
J
Java Code Geeks
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
Last Week in AI
Last Week in AI

博客园 - CHHC

从服务端到桌面端:Go + HTML 多端交付方案 Dify 网页爬虫 Ai常用工具 Dify公司内部知识库权限隔离 智能体:OA行政助手 dify4: test-doc dify3: test-api dify2: test-image dify1: test-local-ollama 访问本地大模型 docker + dify + ollma 安装与配置 dify使用 eSIM SGP32 IPAd 适配移远EC200A OpenCPU 公钥解析 eSIM SGP32 自建符合GSMA规范的eIM平台(支持SGP32及SGP22卡接入) eSIM SGP32 eIM GSMA转码器(支持net及java) eSIM SGP32 证书 eSIM SGP32 生成eUICC所需的配置数据 eSIM SGP32 EuiccPackage包eimSignature和euiccSignEPR生成及校验 eSIM SGP32/SGP22 EUICC.SDK - IPAd RTSPShape-包含服务端及客户端 树莓派安装与配置 NetCore树莓派桌面应用程序 C#解析TLV数据(der -> asn1) golang 项目依赖备份 SGP32笔记 SoftSIM - swSIM eSIM SGP.22 LPA程序开发 - 协议解析 eSIM SGP.22 LPA程序开发 - 实现功能 PGP文件加解密 vscode配置c/c++环境
跨平台桌面应用(内嵌 Web 界面 + Go RESTful API 服务)
CHHC · 2026-09-05 · via 博客园 - CHHC

程序启动后在一个原生 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

image

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

image