在上一篇文章中,我介绍了 Mole 客户端的业务闭环。今天,我们切入代码层面,聊聊基于 Wails 3 的 Go 后端是如何驱动 frp 核心并保证体验平滑的。

一、 Wails 3 项目启动架构

Wails 3 的启动逻辑高度模块化。在 main.go 中,一切从 application.New 开始。

1. 资产集成与服务绑定

首先,我们需要通过 embed.FS 将前端生成物(Vite 构建后的 dist 目录)打包进二进制文件:

//go:embed all:frontend/dist
var assets embed.FS

func main() {
    app := application.New(application.Options{
        Name:        "Mole",
        Description: "Fast Reverse Proxy Manager",
        Services: []application.Service{
            application.NewService(&FrpService{}), // 绑定核心服务
        },
        Assets: application.AssetOptions{
            Handler: application.AssetFileServerFS(assets),
        },
    })

    // 创建窗口逻辑...
}

关键点: Services 是 Wails 3 的精髓。它是一个切片,支持绑定多个服务实例。每个服务中定义的公开方法,前端都可以通过自动生成的 JS 绑定直接调用。

二、 核心经验总结:从“能跑”到“好用”

在开发 Mole 的原型过程中,我踩了不少坑,总结出以下 5 个核心经验:

经验 1:系统托盘与“假关闭”逻辑

对于穿透工具,用户习惯点击“关闭”后应用依然在后台运行。
设置: 在 application.WindowOptions 中将 DisableQuitOnLastWindowClosed 设为 true。
事件监听: 监听 WindowsClosing 事件。当用户点击关闭图标时,调用 window.Hide() 而非销毁窗口。这样配合系统托盘(System Tray),可以实现应用的常驻运行。

经验 2:OnShutdown 与资源回收的“终点站”

最初我尝试在 Service 的 OnShutdown 中释放资源,但在实现托盘模式后,发现生命周期管理变得复杂。
最佳实践: 在 main.go 的应用级别配置 OnShutdown 回调。在这里统一清理 frp 进程、临时文件或断开云端连接,能确保程序退出时干干净净。

经验 3: ServiceStartup:预加载的黄金位置

每个 Service 都可以实现 ServiceStartup(ctx context.Context, options application.ServiceOptions) error 接口。
在 Mole 中,我利用这个阶段完成以下任务:

  1. 读取本地 config.yaml。
  2. 从云端同步最新的子域名与 Token。
  3. 初始化内部状态机。

经验 4: 完美封装:Embed 编译与二进制调用

为了实现“单文件分发”,我将 frpc 原始二进制文件也通过 embed.FS 打包。
优化:在交叉编译时,根据目标平台(Windows/macOS/Linux)选择性打包对应的 frpc,避免生成物过于臃肿。
静默运行(Windows 特供): 启动 exec.Command 时,必须添加 syscall.SysProcAttr{HideWindow: true}。这能彻底隐藏那个令人生厌的命令行黑窗口。

经验 5: 实时日志流:双 Goroutine 与事件机制

这是用户感知最强的功能。如何将 frp 的输出实时显示在前端?
管道截获: 获取 exec.Command 的 StdoutPipe 和 StderrPipe。
并发读取: 开启两个 Goroutine 分别读取这两个管道,防止阻塞。
事件推送: 读取到内容后,利用 app.Emit(“frp-logs”, content) 将数据推送到前端。前端只需要监听 frp-logs 事件,即可实现类似 Linux tail -f 的丝滑体验。

三、 代码片段预演:启动进程并捕获日志

func (s *FrpService) StartFrp(configPath string) {
    cmd := exec.Command(s.frpBinPath, "-c", configPath)

    // Windows 隐藏窗口
    s.setHideWindowAttributes(cmd)

    stdout, _ := cmd.StdoutPipe()

    go func() {
        reader := bufio.NewReader(stdout)
        for {
            line, err := reader.ReadString('\n')
            if err != nil { break }
            // 触发 Wails 全局事件
            s.app.Emit("frp-logs", line)
        }
    }()

    cmd.Start()
}

四、 结语

Wails 3 提供的 Services 和 Events 机制,极大地简化了 Go 与前端的通信。通过合理的生命周期管理(Startup/Shutdown)和进程控制,我们成功地为 frp 这个硬核工具披上了一层优雅的“外衣”。

下一篇预告:
我们将转战前端,聊聊如何用 JS 配合 Wails 事件流构建实时日志看板,以及如何实现那个关键的“小程序激励视频”弹窗交互。