用 Wails + Go + Vue3 开发工具,聊聊踩过的坑
背景交代:用Wails开发桌面软件,覆盖图片、音频、视频、电子书、文档、数据六大类。聊聊技术选型和实现过程中遇到的一些问题,顺便给考虑用 Wails 做桌面应用的同学一些参考。
为什么选 Wails 而不是 Electron
这个问题几乎是第一个面对的选择。Electron 生态更成熟,社区大,VS Code、Figma、Slack 都在用。但对我来说有几个问题:
打包体积:一个 Electron 应用的最小体积大概是 80-150MB,因为要把 Chromium 和 Node.js 都打进去。Wails 打包出来的 .exe 通常在 15-30MB 左右,差距挺大的。
内存占用:Electron 应用的内存基线比较高,一个简单的工具应用跑起来就要 150-200MB 内存,Wails 因为用系统 WebView,基线低得多。
技术栈:后端我已经确定用 Go,Wails 的架构天然适合,前端可以用 Vue3/React,跟 Web 开发体验一致。
当然 Wails 也有代价:WebView 在不同 Windows 版本上的表现有细微差异(依赖系统的 WebView2),某些 CSS 特性在老 Windows 上可能有问题。不过 WebView2 现在已经随 Windows 11 内置了,Windows 10 的覆盖率也在提升。
项目结构
bash
ConvertET/
├── main.go # Wails 入口
├── app.go # 前端调用的 RPC 接口
├── backend/
│ ├── converters/ # 各类格式转换器
│ │ ├── image/
│ │ ├── audio/
│ │ ├── video/
│ │ ├── ebook/
│ │ ├── document/
│ │ └── data/
│ ├── services/ # 业务逻辑
│ ├── models/ # 数据结构定义
│ └── utils/ # 工具函数
└── frontend/ # Vue3 前端
└── src/
├── components/
├── stores/ # Pinia 状态管理
└── locales/ # i18n 国际化文件
Wails 的前后端通信模型:Go 里面暴露的方法,在 JS 里可以直接作为异步函数调用,Wails 帮你处理了序列化和 IPC。比如:
go
// Go 端定义
func (a *App) StartConversion(request models.ConversionRequest) *models.Result {
return a.appService.StartConversion(request)
}
javascript
// Vue 里直接调用
import { StartConversion } from '../wailsjs/go/main/App'
const result = await StartConversion({
inputPath: filePath,
targetFormat: 'mp4',
options: { quality: 'high' }
})
这个 DX(开发者体验)是真的不错,省去了定义 IPC channel、事件处理这些 Electron 里的样板代码。
图片转换:为什么不用 ImageMagick
图片转换这块,我最初的想法是调 ImageMagick,功能强大,格式支持广。但有个问题:用户需要单独安装 ImageMagick 并配置环境变量,这对普通用户来说门槛很高。
最后选择了纯 Go 实现:
go
import (
"image/jpeg"
"image/png"
"golang.org/x/image/bmp"
"golang.org/x/image/webp"
"github.com/disintegration/imaging"
)
image/jpeg、image/png、image/gif:标准库golang.org/x/image/bmp、golang.org/x/image/webp:官方扩展库github.com/disintegration/imaging:提供高质量缩放(Lanczos 算法)
这套组合覆盖了 JPEG/PNG/BMP/WEBP/GIF 五种格式,不依赖任何外部可执行文件,打包进二进制直接用。
唯一的遗憾是 WEBP 编码:golang.org/x/image/webp 只支持解码,不支持编码。所以目前转到 WEBP 格式的输出实际上是 PNG。这是 Go 标准库的历史局限,正在考虑引入 libwebp 或者找纯 Go 实现的方案。
有个坑:文件扩展名和实际格式不符
用户传进来的文件,扩展名未必和实际格式一致。我见过把 PNG 文件命名成 .jpg 的,也见过把 JPEG 改成 .png 的。
所以图片加载时,我加了 magic bytes 检测:
go
func detectImageFormat(r io.Reader) string {
header := make([]byte, 12)
n, _ := r.Read(header)
if n < 2 {
return ""
}
switch {
case header[0] == 0xFF && header[1] == 0xD8:
return "jpeg"
case header[0] == 0x89 && header[1] == 0x50:
return "png"
case header[0] == 0x47 && header[1] == 0x49:
return "gif"
case header[0] == 0x42 && header[1] == 0x4D:
return "bmp"
case header[0] == 0x52 && header[1] == 0x49:
return "webp" // RIFF 容器
}
return ""
}
读文件头的前几字节判断格式,优先级高于扩展名,避免因为错误的文件扩展名导致解码失败。
进度回调的设计
格式转换可能要跑几秒到几分钟,UI 需要实时显示进度。Wails 提供了事件机制:
go
// 在转换器里调用回调
type ProgressCallback func(progress int, message string)
// 回调函数通过 Wails runtime 发送事件到前端
callback := func(progress int, message string) {
runtime.EventsEmit(ctx, "conversion:progress", map[string]interface{}{
"taskID": taskID,
"progress": progress,
"message": message,
})
}
前端监听事件:
javascript
import { EventsOn } from '../wailsjs/runtime'
EventsOn('conversion:progress', (data) => {
task.progress = data.progress
task.statusMessage = data.message
})
这个模式用下来挺好,进度条更新流畅,没有明显的 UI 卡顿。
国际化:18 种语言怎么维护
前端用 vue-i18n,语言文件放在 locales/ 目录下,每种语言一个 JSON 文件。系统语言自动检测这块,在 stores/app.ts 里用 navigator.language 做了一个语言代码到 locale key 的映射。
维护 18 种语言翻译是个体力活。目前英文是精确翻译,其他语言用了机器翻译初版 + 部分人工校对。欢迎有多语言能力的朋友来帮忙。
最后
Wails 对于做 Go 生态的桌面工具来说是个很好的框架,前后端分离清晰,WebView 渲染效果好(Vuetify 这类 Material Design 框架跑得很顺)。社区没有 Electron 大,但文档比较完善,遇到问题去 GitHub Issues 基本都能找到答案。