这篇要解决的问题
写 Go 的团队迟早会遇到一个需求:给后端服务配一个桌面客户端,或者把内部工具做成双击就能跑的程序。这时候摆在你面前的通常是 Electron——它带一整个 Chromium 运行时,产物上百 MB、内存占用大;或者是 Tauri——同样是系统 WebView 方案,但后端语言是 Rust,Go 团队得为它再学一门语言。Wails 是第三条路:前端照常写 HTML/CSS/JS(或任意框架),后端直接用 Go,二者通过方法绑定互相调用。这篇按”装起来、跑通一个窗口、看懂它和 Electron/Tauri 的本质差异、判断要不要用”的顺序走一遍。写作时 v2 线的最新版是 2.9.2(2024 年 11 月初发布),v3 还在 alpha 阶段,本文全部基于 v2。
安装与第一个窗口
Wails 对环境的要求是 Go、Node.js,以及对应平台自带的 WebView:Windows 10/11 需要 WebView2 运行时(新版系统通常已内置),macOS 无额外要求,Linux 需要 webkit2gtk 的开发包。Go 与 Node 的最低版本要求以 wails.io 安装页为准。装 CLI 和初始化项目:
# 安装 Wails 命令行工具
go install github.com/wailsapp/wails/v2/cmd/wails@latest
1. 体检:检查各平台依赖是否齐了,缺什么它会告诉你装什么
wails doctor
1. 用 vue-ts 模板建项目;也支持 react、svelte、vanilla 等模板
wails init -n demo -t vue-ts
1. 开发模式:起 Vite 热更新 + Go 后端,改前端代码浏览器直接刷新
wails dev
wails build 会把前端构建产物和 Go 二进制打包成一个可执行文件,输出在 build/bin 下。定性地说,这个产物是十几 MB 的量级,而同等功能的 Electron 应用普遍上百 MB——差的这部分就是没有打包 Chromium 的成本。具体数字随应用内容浮动,以你自己构建的结果为准。
Go 侧与前端怎么接上
模板生成的 main.go 长这样,核心是三件事:嵌入前端资产、声明生命周期回调、把 Go 对象绑定给前端:
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
// 把前端构建产物整个嵌进二进制,发布时只有一个可执行文件
//go:embed all:frontend/dist
var assets embed.FS
type App struct {
ctx context.Context
}
// OnStartup 拿到运行时上下文,之后才能用窗口、事件等能力
func (a *App) startup(ctx context.Context) { a.ctx = ctx }
// 任何导出方法都会暴露给前端,参数和返回值自动做 JSON 序列化
func (a *App) Greet(name string) string {
return "Hello, " + name + "!"
}
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "demo",
Width: 800,
Height: 600,
AssetServer: &assetserver.Options{
Assets: assets, // 页面请求都由这个内嵌资产服务响应
},
OnStartup: app.startup,
Bind: []interface{}{app}, // 绑定后前端才能调 Greet
})
if err != nil {
log.Fatal(err)
}
}
前端侧不用手写调用代码,wails dev 会自动生成 wailsjs/go/main/App.js 绑定模块,import 进来就是类型齐全的函数:
import { Greet } from '../wailsjs/go/main/App';
// 看起来是本地函数调用,实际跨了 JS 到 Go 的一次绑定调用
const result = await Greet('Wails');
反模式点名:把业务逻辑写在前端 JS 里、Go 只当文件读写工具。这样等于放弃 Go 的类型和并发能力,客户端代码也没法复用服务端逻辑。合理的分界是:状态管理和交互留前端,数据加工、系统调用、网络请求放 Go 侧,前端只消费绑定的方法。
除了方法绑定,双向还有事件通道:Go 侧任何时刻都能向前端广播事件,长任务的进度推送就靠它,而不是让前端轮询:
// Go 侧(import "github.com/wailsapp/wails/v2/pkg/runtime"):
// 处理完一段就发一条事件,载荷跟在事件名后面
runtime.EventsEmit(a.ctx, "task:progress", done, total)
// 前端侧:EventsOn 订阅同名事件
import { EventsOn } from '../wailsjs/runtime';
EventsOn("task:progress", (done, total) => updateBar(done, total));
v2 还内置了系统对话框、菜单、剪贴板等常用桌面能力的封装,多数工具型应用不用碰 CGO。要知道的能力边界是:v2 是单窗口模型,一个进程一个主窗口,多窗口是 v3 才补上的重点能力——如果产品形态必须多窗口,现在只能选 Electron/Tauri 或等 v3 稳定。
与 Electron/Tauri 的思路差异
| 维度 | Electron | Tauri | Wails |
|---|---|---|---|
| 页面渲染载体 | 自带 Chromium,跨平台渲染一致 | 系统 WebView | 系统 WebView |
| 后端语言 | Node.js | Rust | Go |
| 产物体积 | 大(打包整个运行时) | 小 | 小(十几 MB 量级) |
| 渲染一致性 | 完全可控 | 随系统 WebView 而异 | 随系统 WebView 而异 |
| 团队上手成本 | 低(全 JS 生态) | 需 Rust 背景 | 低(Go 背景即可) |
真正的取舍在”渲染一致性”这一行:Electron 用体积换来了所有平台同一版本的 Chromium,页面不会因为客户机器的 WebView 版本旧而样式错乱;Wails 和 Tauri 换来了小体积和原生语言后端,代价是要接受各家系统 WebView 的行为差异。工具型界面(表单、列表、配置面板)对此基本无感;如果你要做重度依赖最新 CSS 特性的界面,需要先评估目标用户的系统版本。
什么时候选它
三种场景值得直接选 Wails:团队主力是 Go、不想为桌面端引入第二门后端语言;做的是内网工具、运维面板、数据看板这类”轻界面 + 重后端”应用;在意安装包体积和内存占用。反过来两种情况要慎重:一是界面要求像素级一致的复杂渲染(先实测 WebView 兼容性),二是想现在就上 v3——它还在 alpha,API 未定型,生产项目请留在 v2。诚实地说,Wails 的社区规模小于 Electron 和 Tauri,遇到偏门问题的检索成本更高,这点要有预期。
延伸阅读:Go 侧代码的基础语法见《第一个 Go 程序》;绑定方法里跑后台任务时用到的并发原语见《Go 并发基础》;Greet 方法背后 Go 标准库的分工见《Go标准库详解》。
版本核实(以官方文档为准):写作时 Wails v2 线最新版为 2.9.2(2024 年 11 月初发布),v3 处于 alpha 阶段未发布正式版;本文命令与 API(wails doctor/init/dev/build、embed 资产嵌入、OnStartup、Bind、自动生成的 wailsjs 绑定)均为 v2 已发布行为,细节以 wails.io 当前文档为准。