
1. 为什么我最终选了 Wails v2 而不是 Electron1.1 一个真实的需求场景去年年底我接了个私活帮一个做跨境电商的朋友团队做一个内部用的 AI 助手桌面工具。需求说起来不复杂能调用大模型接口做文案生成、图片批量处理、本地文件整理还要能读取他们内部的一套商品数据做分析。但有个硬性约束——必须是一个能双击打开、不依赖浏览器、能访问本地文件系统的桌面应用而且他们团队清一色 Windows 办公环境Mac 只有两台。我第一反应是 Electron毕竟生态成熟、资料多、招人也好招。但真算了一下账就犹豫了一个空壳 Electron 应用打包出来就 150MB 起步加上 Vue3、各种依赖轻松突破 250MB。更要命的是内存占用开三个窗口就能吃掉 800MB 内存他们那些办公电脑普遍是 8GB 内存的旧机器跑起来风扇狂转。后来我把目光转向了Wails v2。简单说Wails 是一个用 Go 做后端、用前端框架Vue3、React、Svelte 都行做界面的桌面应用框架。它不像 Electron 那样把整个 Chromium 打包进去而是复用系统自带的 WebView——Windows 上用 WebView2macOS 上用 WKWebViewLinux 上用 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.invokeipcMain.handle要直观太多。1.3 技术选型的对比账我把当时做的选型对比整理成表格方便你直接参考维度Wails v2ElectronTauri后端语言GoNode.jsRust前端框架任意任意任意包体积空壳约 8-12MB约 150MB约 5-10MB内存占用单窗口约 80-120MB约 250-400MB约 60-100MB系统 WebView 依赖需要不需要需要学习曲线低会 Go 就行中高Rust生态成熟度中高中跨平台编译需分平台需分平台需分平台我最终选 Wails 的决定性因素是团队里有两个 Go 后端前端只有一个 Vue3 开发。用 Wails 的话Go 那两位可以直接上手写业务逻辑前端只需要专注界面分工特别清晰。如果用 ElectronGo 那两位得先学 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 CLIgo install github.com/wailsapp/wails/v2/cmd/wailslatest这里有个坑要注意go install会把二进制装到$GOPATH/bin或者$HOME/go/bin你得确保这个目录在 PATH 里。Windows 上默认是%USERPROFILE%\go\binLinux 上是~/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): Promiseservices.ChatResponse;你在 Vue3 组件里直接 import 就能用script setup langts 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 上是.exemacOS 上是.appLinux 上是无扩展名的二进制。我实测一个带 Vue3 Element Plus 几个业务模块的项目Windows 下打包出来大概 12MB压缩后 5MB 左右发给客户直接就能跑。如果你想减小体积可以在wails build时加-trimpath和-ldflags-s -w能去掉调试信息和符号表体积能再小 20% 左右wails build -trimpath -ldflags-s -w4.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 的 PNGWails 会自动生成各平台需要的尺寸。Windows 上如果要生成安装包我推荐用 Inno Setup配置简单、免费、体积小。一个基础的.iss脚本大概长这样[Setup] AppNameAI 桌面助手 AppVersion1.0.0 DefaultDirName{autopf}\AIDesktop OutputBaseFilenameAIDesktopSetup Compressionlzma2 SolidCompressionyes [Files] Source: build\bin\my-ai-app.exe; DestDir: {app} [Icons] Name: {autoprograms}\AI 桌面助手; Filename: {app}\my-ai-app.exemacOS 上则是用create-dmg或者直接打 zip 分发。如果要上架 App Store那还得处理签名、公证、沙盒权限那一堆事这个坑比较深建议单独查资料。注意Windows 上如果没做代码签名用户第一次运行会弹 SmartScreen 警告。这个对内部工具来说问题不大但如果要对外分发建议买个代码签名证书能省掉很多用户咨询。5. 踩过的坑与排查经验5.1 开发阶段最常见的五个问题我把实际开发中遇到的问题整理成速查表问题现象可能原因解决方法窗口白屏WebView2 未安装或版本过低安装最新 WebView2 Runtimewails dev启动报错找不到前端前端依赖未安装进 frontend 目录跑 npm installGo 方法前端调用不到方法未导出或未注册检查首字母大写、检查 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 约 90MBElectron 约 280MB。内存占用加载 1000 条列表数据Wails 约 150MBElectron 约 420MB。打包体积Wails 约 12MBElectron 约 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 值得认真考虑。