最近在折腾 Wails v3。
前面写过一篇 Wails 和 Electron 的对比,里面提到一个很直观的点:Electron 很成熟,但运行时也确实重;Wails 走的是 Go + 系统 WebView 的路线,理论上会轻很多。
但光说“更轻”没什么意思。
所以这次我干脆写了一个小工具,把它真的打包出来,看一下一个 Wails v3 桌面应用到底能做到多大。
最后做出来的是一个本地开发者工具箱,打包后的 Windows exe 是:
8.70MB
不是压缩包,也不是前端 dist 目录,而是实际生成的 json-tool-wails.exe。
做了个什么东西
这个工具箱很简单,放了几个我平时开发里经常会用到的小功能:
| 工具 | 功能 |
|---|---|
| JSON | 格式化 / 压缩 |
| Base64 | 编码 / 解码 |
| 时间戳 | 秒 / 毫秒转换 |
| URL | 编码 / 解码 |
| UUID | 批量生成 |
我没有把它做成一个很复杂的应用。
原因也很简单:这次主要想验证 Wails 做轻量桌面工具的体验,不是要一上来做一个大而全的软件。
这种小工具有一个特点:单个功能都不难,但组合到一起以后,确实会比临时打开一堆网页方便一点。
尤其是处理接口返回、Token、配置片段这类内容时,我个人会更倾向于放在本地工具里处理。

项目结构
这次用的是 Wails v3 + Vue。
创建项目:
wails3 init -n my-tool-wails -t vue-ts
项目结构大概长这样:
my-tool-wails
├─ main.go
├─ frontend
│ ├─ src
│ │ ├─ App.vue
│ │ └─ main.ts
│ └─ package.json
├─ build
└─ Taskfile.yml
可以粗略理解成:
main.go负责桌面应用入口和窗口frontend负责界面- Wails 负责把 Go 和前端应用串起来
这次几个转换功能都比较轻,所以逻辑直接写在前端里。后面如果要做文件读取、目录扫描、日志分析这种,就更适合交给 Go 来做。
JSON 格式化
JSON 是第一个工具。
输入一段 JSON,可以格式化,也可以压缩成一行。

核心代码没什么花活:
function formatJson() {
try {
setResult(JSON.stringify(JSON.parse(input.value), null, 2))
} catch (err) {
setFailure(err, 'JSON 解析失败')
}
}
function minifyJson() {
try {
setResult(JSON.stringify(JSON.parse(input.value)))
} catch (err) {
setFailure(err, 'JSON 解析失败')
}
}
这类功能放在前端就够了,不需要绕到 Go 后端。
Base64 编码和解码
第二个是 Base64。
这个功能我用得还挺多,尤其是临时看一些配置、Token、调试字符串的时候。

这里要注意中文。
如果直接用 btoa 和 atob,遇到中文会翻车,所以我做了一层 UTF-8 处理:
function encodeBase64() {
try {
setResult(btoa(unescape(encodeURIComponent(input.value))))
} catch (err) {
setFailure(err, 'Base64 编码失败')
}
}
function decodeBase64() {
try {
setResult(decodeURIComponent(escape(atob(input.value.trim()))))
} catch (err) {
setFailure(err, 'Base64 解码失败')
}
}
这不是什么复杂实现,但放到本地工具里,用起来会安心一点。
时间戳转换
时间戳转换也是开发里经常遇到的东西。
有的接口返回秒,有的接口返回毫秒,有时候还要转成本地时间看一眼。

我这里做了一个很朴素的判断:
- 10 位以内按秒处理
- 其他情况按毫秒处理
function parseTimestamp() {
try {
const raw = input.value.trim()
const numeric = Number(raw)
if (!Number.isFinite(numeric)) {
throw new Error('请输入数字时间戳')
}
const milliseconds = raw.length <= 10 ? numeric * 1000 : numeric
const date = new Date(milliseconds)
setResult(
[
`本地时间:${date.toLocaleString()}`,
`ISO 时间:${date.toISOString()}`,
`秒:${Math.floor(milliseconds / 1000)}`,
`毫秒:${Math.floor(milliseconds)}`,
].join('\n'),
)
} catch (err) {
setFailure(err, '时间戳转换失败')
}
}
这类功能很适合放进工具箱里,不大,但高频。
URL 编码和解码
URL 编码主要用来处理查询参数。
比如 URL 里有中文、空格、特殊字符时,临时转一下会方便很多。

代码也很直接:
function encodeUrl() {
try {
setResult(encodeURIComponent(input.value))
} catch (err) {
setFailure(err, 'URL 编码失败')
}
}
function decodeUrl() {
try {
setResult(decodeURIComponent(input.value))
} catch (err) {
setFailure(err, 'URL 解码失败')
}
}
UUID 批量生成
最后是 UUID。

浏览器已经有 crypto.randomUUID(),所以实现很省事:
function generateUuid() {
const count = Math.min(Math.max(Math.floor(uuidCount.value), 1), 50)
uuidCount.value = count
setResult(Array.from({ length: count }, () => crypto.randomUUID()).join('\n'))
}
这里我限制最多生成 50 个,避免手滑生成一大屏。
Wails 是怎么打包前端的
Wails 有一个地方我挺喜欢:它会把前端构建产物嵌入到 Go 程序里。
main.go 里有这样一段:
//go:embed all:frontend/dist
var assets embed.FS
也就是说,Vue 构建出来的 frontend/dist 会被嵌进最终的 Go 可执行文件。
最后生成的就是一个 exe。
这也是为什么这种小工具用 Wails 会比较舒服:写界面还是前端那套,分发时又可以走 Go 可执行文件的方式。
打包结果
打包命令:
wails3 build
构建完成后,产物在:
bin/my-tool-wails.exe
我的测试环境:
| 项目 | 版本 |
|---|---|
| 系统 | Windows |
| Wails | v3.0.0-alpha.78 |
| Go | 1.24.0 |
| Node | v24.14.1 |
| npm | 11.11.0 |
| 前端 | Vue + Vite |
最终体积:
| 产物 | 体积 |
|---|---|
my-tool-wails.exe |
8.70MB |
准确字节数:
9120768 bytes
这个数字只代表我当前这个 Demo 和这套打包配置,不代表所有 Wails 项目都会是这个大小。
但至少对我来说,它已经足够说明问题:做轻量桌面工具,Wails 的基础成本确实很低。
适合做什么
写完这个工具以后,我对 Wails 的感觉更明确了一点。
它不一定适合所有复杂桌面产品,但很适合这类场景:
- 内部运维小工具
- 本地开发者工具箱
- Go CLI 工具的桌面版
- 文件处理工具
- 日志查看工具
- 配置生成工具
- 数据转换工具
尤其是你已经有 Go 代码的时候,Wails 会很顺。
以前一个 Go 工具可能只能在命令行里跑。现在用 Wails 套一层前端界面,就能变成一个普通用户也能打开的桌面应用。
总结
这次我用 Wails v3 做了一个本地开发者工具箱。
功能不复杂,但比 Hello World 更接近真实使用场景。
最后打包出来的 Windows exe 是 8.70MB。
我的感受是:
如果你是 Go 开发者,想做一个轻量、本地、可扩展的桌面工具,Wails v3 确实值得试一下。
下一篇我准备继续往下做:让 Vue 前端调用 Go 后端方法,看看 Wails 里的前后端通信到底怎么写。