1. 为什么我最终选了 Wails v2 而不是 Electron
1.1 一个真实的需求场景
去年年底我接了个私活,帮一个做跨境电商的朋友团队做一个内部用的 AI 助手桌面工具。需求说起来不复杂:能调用大模型接口做文案生成、图片批量处理、本地文件整理,还要能读取他们内部的一套商品数据做分析。但有个硬性约束——必须是一个能双击打开、不依赖浏览器、能访问本地文件系统的桌面应用,而且他们团队清一色 Windows 办公环境,Mac 只有两台。
我第一反应是 Electron,毕竟生态成熟、资料多、招人也好招。但真算了一下账就犹豫了:一个空壳 Electron 应用打包出来就 150MB 起步,加上 Vue3、各种依赖,轻松突破 250MB。更要命的是内存占用,开三个窗口就能吃掉 800MB 内存,他们那些办公电脑普遍是 8GB 内存的旧机器,跑起来风扇狂转。
后来我把目光转向了Wails v2。简单说,Wails 是一个用 Go 做后端、用前端框架(Vue3、React、Svelte 都行)做界面的桌面应用框架。它不像 Electron 那样把整个 Chromium 打包进去,而是复用系统自带的 WebView——Windows 上用 WebView2,macOS 上用 WKWebView,Linux 上用 WebKitGTK。这一下就把包体积压到了 10MB 以内,内存占用也降到了 Electron 的三分之一左右。
提示:Wails 的核心卖点不是"功能比 Electron 多",而是"用更小的代价做同样的事"。如果你的项目重度依赖 Chromium 特有的 API,或者需要精确控制渲染进程,那 Electron 仍然更合适。但如果是常规的业务型桌面应用,Wails 的性价比高得多。
1.2 Wails v2 到底解决了什么问题
我把 Wails v2 的价值总结成三句话:
- 对 Go 开发者:不用学 JavaScript 生态那一堆构建工具,用自己熟悉的 Go 写业务逻辑,前端交给专业的人或者用现成模板。
- 对前端开发者:不用碰 Node.js 的原生模块编译、不用处理 Electron 的主进程/渲染进程通信那套复杂模型,直接调用 Go 暴露的方法就行。
- 对项目本身:编译产物小、启动快、内存低,分发的时候一个 exe 文件丢过去就能跑。
Wails v2 的通信模型特别值得说一下。它在 Go 侧和前端侧之间建立了一个绑定层,你在 Go 里定义一个结构体方法,通过wails.Run注册进去,前端就能直接await window.go.你的包名.你的结构体.你的方法()调用。返回值、错误、甚至 Go 的 context 都能透传。这比 Electron 的 IPC 那套ipcRenderer.invoke+ipcMain.handle要直观太多。
1.3 技术选型的对比账
我把当时做的选型对比整理成表格,方便你直接参考:
| 维度 | Wails v2 | Electron | Tauri |
|---|---|---|---|
| 后端语言 | Go | Node.js | Rust |
| 前端框架 | 任意 | 任意 | 任意 |
| 包体积(空壳) | 约 8-12MB | 约 150MB+ | 约 5-10MB |
| 内存占用(单窗口) | 约 80-120MB | 约 250-400MB | 约 60-100MB |
| 系统 WebView 依赖 | 需要 | 不需要 | 需要 |
| 学习曲线 | 低(会 Go 就行) | 中 | 高(Rust) |
| 生态成熟度 | 中 | 高 | 中 |
| 跨平台编译 | 需分平台 | 需分平台 | 需分平台 |
我最终选 Wails 的决定性因素是:团队里有两个 Go 后端,前端只有一个 Vue3 开发。用 Wails 的话,Go 那两位可以直接上手写业务逻辑,前端只需要专注界面,分工特别清晰。如果用 Electron,Go 那两位得先学 Node.js 那一套,时间成本划不来。
2. 环境搭建:从零到能跑起来的完整步骤
2.1 Go 环境与 Wails CLI 安装
先说 Go 环境。Wails v2 要求 Go 1.18 以上,我建议直接用 1.21 或更高版本,因为一些新特性(比如slices包)用起来更顺手。Windows 上装 Go 最简单的方式是去官网下 msi 安装包,一路下一步就行。装完在命令行敲go version,能输出类似go version go1.21.5 windows/amd64就说明成功了。
Linux 上稍微麻烦一点。以 Ubuntu 为例,我习惯用官方压缩包而不是 apt 源里的版本,因为源里的往往偏旧:
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc source ~/.bashrc go version装完 Go 之后装 Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@latest这里有个坑要注意:go install会把二进制装到$GOPATH/bin或者$HOME/go/bin,你得确保这个目录在 PATH 里。Windows 上默认是%USERPROFILE%\go\bin,Linux 上是~/go/bin。装完敲wails version,能输出版本号就对了。
注意:如果你在国内网络环境下
go install卡住,可以配置 Go 的模块代理。这个属于常规开发环境配置,具体方式参考 Go 官方文档即可。
2.2 系统 WebView 依赖的处理
这是 Wails 相比 Electron 唯一"麻烦"的地方——它依赖系统 WebView,所以不同平台要装不同的东西。
Windows:需要 WebView2 Runtime。Win11 自带,Win10 较新的版本也自带,但一些老版本没有。我建议直接在安装包里带上 WebView2 的引导安装程序,Wails 打包时支持这个配置。开发阶段如果发现窗口打不开或者白屏,八成就是缺 WebView2,去微软官网下个 Evergreen Bootstrapper 装上就行。
macOS:系统自带 WKWebView,什么都不用装。但要注意 Xcode Command Line Tools 得装好,因为编译过程需要。
Linux:这个最麻烦,需要 WebKitGTK。以 Ubuntu 22.04 为例:
sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev如果是 Fedora 或者 Arch,包名不一样,具体查一下发行版文档。我实测下来 Ubuntu 22.04 和 24.04 都挺顺的,Debian 12 也没问题。
2.3 创建第一个 Wails 项目
环境齐了之后,创建项目就一行命令:
wails init -n my-ai-app -t vue-n是项目名,-t vue指定用 Vue 模板。Wails 官方提供了 vue、vue-ts、react、svelte 等模板,我建议用vue-ts,因为 TypeScript 在调用 Go 方法时能给你类型提示,体验好很多:
wails init -n my-ai-app -t vue-ts创建完之后目录结构大概是这样:
my-ai-app/ ├── build/ # 打包相关资源 ├── frontend/ # Vue3 前端项目 │ ├── src/ │ ├── package.json │ └── vite.config.ts ├── app.go # 主应用逻辑 ├── main.go # 入口 ├── wails.json # Wails 配置 └── go.mod进到项目目录,跑wails dev,第一次会下载前端依赖并编译,等一会儿就能看到一个窗口弹出来。这个开发模式支持热重载——改前端代码窗口自动刷新,改 Go 代码会自动重新编译并重启应用,体验相当顺滑。
2.4 Vue3 前端的依赖安装细节
Wails 的 Vue 模板默认用的是 Vite 构建,前端目录是独立的 npm 项目。进frontend目录跑npm install就行。如果你要用 SCSS,得额外装:
npm install -D sass要用 Element Plus 或者 Naive UI 这类组件库,也是在这个目录里装。我实测下来,Wails 对前端生态完全没限制,你想用什么用什么,Vite 能跑的东西它都能跑。
有个细节值得提一下:Wails 的wails dev启动时,前端其实跑在一个本地 Vite 开发服务器上,Wails 的窗口加载的是这个服务器的地址。所以你在浏览器里直接访问那个地址,也能看到界面,只是window.go那些绑定对象不存在。调试样式的时候用浏览器挺方便的。
3. Go 与 Vue3 的通信:把 AI 能力接进来
3.1 绑定机制的核心原理
Wails v2 的绑定机制说白了就是代码生成 + 运行时桥接。你在 Go 里定义好结构体和方法,Wails 在编译时会扫描这些方法,生成对应的 JavaScript/TypeScript 存根代码,放到frontend/wailsjs/go/目录下。前端 import 这些存根,调用时实际上是通过 WebView 的消息通道把参数序列化成 JSON 传给 Go 侧,Go 执行完再把结果序列化回来。
这个机制有几个关键约束你得记住:
- 方法必须是导出的(首字母大写)。
- 参数和返回值必须是可 JSON 序列化的。Go 的
struct、map、slice、基本类型都没问题,但chan、func、含非导出字段的结构体就不行。 - 方法签名里可以带
context.Context作为第一个参数,Wails 会自动注入,方便你做取消和超时控制。
3.2 定义 AI 服务结构体
我在项目里是这么组织的。先建一个services包,里面放各种业务服务。AI 相关的单独一个文件:
package services import ( "context" "encoding/json" "fmt" "net/http" "bytes" "time" ) type AIService struct { apiKey string baseURL string client *http.Client } type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } type ChatResponse struct { Content string `json:"content"` Usage Usage `json:"usage"` } type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` } func NewAIService(apiKey, baseURL string) *AIService { return &AIService{ apiKey: apiKey, baseURL: baseURL, client: &http.Client{ Timeout: 60 * time.Second, }, } } func (s *AIService) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) { body, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("序列化请求失败: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/v1/chat/completions", bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+s.apiKey) resp, err := s.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("接口返回错误状态: %d", resp.StatusCode) } var result ChatResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } return &result, nil }然后在main.go里注册:
package main import ( "embed" "my-ai-app/services" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) //go:embed all:frontend/dist var assets embed.FS func main() { aiService := services.NewAIService("你的key", "你的接口地址") err := wails.Run(&options.App{ Title: "AI 桌面助手", Width: 1200, Height: 800, AssetServer: &assetserver.Options{ Assets: assets, }, Bind: []interface{}{ aiService, }, }) if err != nil { println("启动失败:", err.Error()) } }3.3 前端调用与类型提示
wails dev跑起来之后,frontend/wailsjs/go/services/AIService.d.ts会自动生成,内容大概是这样:
export function Chat(arg1: services.ChatRequest): Promise<services.ChatResponse>;你在 Vue3 组件里直接 import 就能用:
<script setup lang="ts"> import { ref } from 'vue' import { Chat } from '../../wailsjs/go/services/AIService' import type { services } from '../../wailsjs/go/models' const input = ref('') const output = ref('') const loading = ref(false) async function send() { if (!input.value.trim()) return loading.value = true try { const req: services.ChatRequest = { model: 'gpt-4o-mini', messages: [ { role: 'system', content: '你是一个专业的电商文案助手' }, { role: 'user', content: input.value } ] } const resp = await Chat(req) output.value = resp.content } catch (e) { output.value = '出错了: ' + e } finally { loading.value = false } } </script>这套流程跑通之后,你会发现 Go 和 Vue3 之间的边界特别清晰——Go 负责所有"重"的逻辑(网络请求、文件操作、数据处理),Vue3 负责所有"轻"的交互(界面渲染、用户输入、状态管理)。这种分工比 Electron 那种前后端都在 JavaScript 里搅和要舒服得多。
3.4 流式输出的处理
AI 应用有个绕不开的需求:流式输出。用户不想等 10 秒才看到完整回复,而是希望文字一个字一个字蹦出来。Wails v2 支持从 Go 侧主动向前端发事件,这就是实现流式的关键。
Go 侧:
func (s *AIService) ChatStream(ctx context.Context, req ChatRequest) error { // 发起流式请求,逐块读取 // 每读到一个 chunk,就通过 runtime.EventsEmit 发给前端 for { chunk := readChunk() if chunk == "" { break } runtime.EventsEmit(ctx, "ai:chunk", chunk) } runtime.EventsEmit(ctx, "ai:done", "") return nil }前端:
import { EventsOn } from '../../wailsjs/runtime/runtime' EventsOn('ai:chunk', (chunk: string) => { output.value += chunk }) EventsOn('ai:done', () => { loading.value = false })这个事件机制是 Wails 的隐藏利器。除了流式输出,你还可以用它做进度条、日志推送、后台任务状态更新等等。我实测下来,每秒推几百个事件完全没问题,性能足够。
4. 打包分发:从开发到能交付的最后一公里
4.1 打包命令与产物结构
开发调试没问题之后,打包就一行:
wails build默认会在build/bin/目录下生成可执行文件。Windows 上是.exe,macOS 上是.app,Linux 上是无扩展名的二进制。我实测一个带 Vue3 + Element Plus + 几个业务模块的项目,Windows 下打包出来大概 12MB,压缩后 5MB 左右,发给客户直接就能跑。
如果你想减小体积,可以在wails build时加-trimpath和-ldflags="-s -w",能去掉调试信息和符号表,体积能再小 20% 左右:
wails build -trimpath -ldflags="-s -w"4.2 跨平台编译的现实约束
这里必须说清楚一个事实:Wails 不支持真正的交叉编译。也就是说,你在 Windows 上没法直接编译出 macOS 的可执行文件,反之亦然。原因是它依赖各平台的 WebView 和系统库,编译过程需要链接平台特定的东西。
那怎么办?我的做法是用 CI 或者虚拟机:
- Windows 产物:在 Windows 机器或 Windows 虚拟机上编译。
- macOS 产物:在 Mac 上编译,而且如果要签名和公证,必须用 Mac。
- Linux 产物:在 Linux 机器或容器里编译。
如果团队规模小,最省事的方案是搞一台 Windows 主力开发机 + 一台 Mac mini 做打包机。如果预算有限,用 GitHub Actions 的 matrix 构建也行,三个平台并行编译,产物自动上传。
4.3 图标、版本信息和安装包
Wails 的打包配置都在wails.json和build/目录里。图标放在build/appicon.png,要求是 1024x1024 的 PNG,Wails 会自动生成各平台需要的尺寸。
Windows 上如果要生成安装包,我推荐用 Inno Setup,配置简单、免费、体积小。一个基础的.iss脚本大概长这样:
[Setup] AppName=AI 桌面助手 AppVersion=1.0.0 DefaultDirName={autopf}\AIDesktop OutputBaseFilename=AIDesktopSetup Compression=lzma2 SolidCompression=yes [Files] Source: "build\bin\my-ai-app.exe"; DestDir: "{app}" [Icons] Name: "{autoprograms}\AI 桌面助手"; Filename: "{app}\my-ai-app.exe"macOS 上则是用create-dmg或者直接打 zip 分发。如果要上架 App Store,那还得处理签名、公证、沙盒权限那一堆事,这个坑比较深,建议单独查资料。
注意:Windows 上如果没做代码签名,用户第一次运行会弹 SmartScreen 警告。这个对内部工具来说问题不大,但如果要对外分发,建议买个代码签名证书,能省掉很多用户咨询。
5. 踩过的坑与排查经验
5.1 开发阶段最常见的五个问题
我把实际开发中遇到的问题整理成速查表:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 窗口白屏 | WebView2 未安装或版本过低 | 安装最新 WebView2 Runtime |
wails dev启动报错找不到前端 | 前端依赖未安装 | 进 frontend 目录跑 npm install |
| Go 方法前端调用不到 | 方法未导出或未注册 | 检查首字母大写、检查 Bind 配置 |
| 修改 Go 代码不生效 | 编译缓存问题 | 删掉 build/bin 重新 build |
| 打包后资源加载失败 | 前端资源路径问题 | 检查 vite.config.ts 的 base 配置 |
其中"窗口白屏"是最常见的。我遇到过好几次,都是因为测试机没装 WebView2。后来我在应用启动时加了个检测,如果 WebView2 不可用就弹个提示引导用户去装,体验好很多。
5.2 生产环境的两个隐蔽陷阱
第一个是路径问题。开发时你的工作目录是项目根目录,但打包后用户双击 exe,工作目录可能是任意位置。所有涉及文件读写的地方,都不能用相对路径,要用os.UserConfigDir()或者os.UserHomeDir()拼绝对路径。我一开始没注意,开发时好好的,打包后用户一保存配置就报错,排查了半天。
第二个是并发问题。Wails 的 Go 侧方法可能被前端并发调用,如果你在方法里操作共享状态(比如一个全局的 map),必须加锁。我有个项目里用了个全局的map[string]*Task存任务状态,没加锁,结果用户快速点几下按钮就 panic 了。后来改成sync.Map才稳定。
5.3 性能调优的几个实测数据
我拿一个中等复杂度的项目做了些性能测试,数据供你参考:
- 冷启动时间:Wails 约 0.8-1.2 秒,Electron 约 2-3 秒。
- 内存占用(空闲):Wails 约 90MB,Electron 约 280MB。
- 内存占用(加载 1000 条列表数据):Wails 约 150MB,Electron 约 420MB。
- 打包体积:Wails 约 12MB,Electron 约 180MB。
这些数据不是绝对的,跟具体项目有关,但量级上的差异是稳定的。对于配置一般的办公电脑,Wails 的体验优势很明显。
5.4 一个关于 AI 接口调用的实用技巧
最后分享一个我在 AI 应用里常用的模式:把接口调用做成可取消的。用户点了"生成"之后如果后悔了,应该能点"停止"。Wails 的绑定方法支持context.Context,前端可以通过window.runtime.EventsEmit触发取消,Go 侧监听这个事件然后 cancel context。
具体做法是 Go 侧维护一个map[string]context.CancelFunc,每次调用生成一个唯一 ID,前端拿着这个 ID 可以取消。这个模式我用了好几个项目,特别稳。用户反馈也特别好,因为 AI 生成有时候确实慢,能中途停止是个很实在的体验提升。
这套 Wails v2 + Vue3 的组合,我从去年用到现在,做了三个项目,最大的一个前端有 40 多个页面、Go 侧有 20 多个服务结构体,跑起来依然很流畅。如果你也在找一个"Go 后端 + 现代前端"的桌面方案,Wails v2 值得认真考虑。