资讯详情

Wails v2 + Go + Vue3:构建轻量级 AI 桌面应用实战

📅 2026/11/2 8:56:16 | 华诺云谱 👁 阅读
Wails v2 + Go + Vue3:构建轻量级 AI 桌面应用实战
桌面端 AI 应用这件事我从 Electron 一路折腾到 Tauri最后在 Wails v2 上停了下来。原因很直接我日常写 Go 比写 Rust 顺手太多而 Wails 恰好把 Go 的后端能力和 Vue3 的前端体验缝在了一起编译出来的东西是单个可执行文件体积比 Electron 小一个数量级内存占用也好看得多。这篇就聊聊怎么用 Wails v2 Go Vue3 搭一个真正能用的 AI 桌面应用——不是那种跑个 Hello World 就完事的 Demo而是带流式对话、本地配置持久化、多会话管理、能打包分发的完整形态。如果你会一点 Go、懂一点 Vue3或者正在找一个比 Electron 更轻的桌面方案下面的内容应该能帮你少走不少弯路。1. 为什么我最终选了 Wails 而不是 Electron 或 Tauri1.1 三个方案摆在一起看差距在哪先把结论放前面选型没有绝对的对错只有场景匹配。我把三个主流方案的关键维度拉了个表你可以对照自己的情况看。维度ElectronTauriWails v2后端语言Node.jsRustGo渲染层Chromium 内置系统 WebView系统 WebView安装包体积80MB 起步3-10MB5-15MB内存占用高多进程低低前端框架任意任意任意Vue/React/Svelte学习曲线低高Rust中Go 友好跨平台全平台全平台全平台Electron 的问题不是不好用是太重。一个空壳应用打包出来就 80MB 起跑起来内存轻松上 300MB。对于 AI 桌面应用这种需要长时间挂着、还要调本地模型或转发请求的场景这个开销是实打实的成本。Tauri 很优秀体积和内存控制得极好但它的后端是 Rust。我不是说 Rust 不好而是对于一个需要快速迭代、频繁改业务逻辑的 AI 应用来说Rust 的编译速度和心智负担会拖慢节奏。写个 HTTP 转发、处理个流式响应Go 的goroutine和channel用起来是真的舒服。Wails v2 的定位刚好卡在中间Go 的后端开发效率 系统 WebView 的轻量渲染。它通过绑定机制把 Go 的结构体方法直接暴露给前端调用前端就像调本地函数一样调 Go这个体验比手动写 IPC 消息协议清爽太多。1.2 Wails 的绑定机制到底怎么工作的很多人第一次用 Wails 会懵前端怎么就能直接await backend.Method()了这背后其实是 Wails 在编译期做了一层代码生成。你在 Go 侧定义一个结构体把它注册到app.Bind()Wails 在wails dev或wails build时会扫描这些方法自动生成对应的 JS/TS 绑定文件放在frontend/wailsjs/go/目录下。前端 import 进来就能直接调用参数和返回值通过 JSON 序列化在 Go 和 WebView 之间传递。这里有个关键点只有导出方法首字母大写才会被绑定而且参数和返回值必须是可 JSON 序列化的类型。我一开始想直接传一个chan string做流式输出结果发现根本绑不了——channel 没法序列化。这个坑后面会专门讲怎么绕。1.3 什么场景下 Wails 是甜点区根据我的实际使用Wails 特别适合这几类应用需要调用本地系统能力读写文件、调用命令行、访问本地数据库Go 的生态在这些方面非常成熟。需要处理并发和网络请求AI 应用大量涉及流式 HTTP、并发任务Go 的并发模型天然适配。团队里有 Go 背景如果后端同学本来就在写 Go前端用 Vue3Wails 能让两边几乎零成本协作。对体积和内存敏感不想让用户为了一个工具装 100MB 的运行时。反过来如果你的应用重度依赖 Chromium 特有的 API或者需要精确控制渲染引擎版本那 Electron 可能更稳。Wails 用的是系统 WebViewWindows 上是 WebView2macOS 上是 WKWebViewLinux 上是 WebKitGTK不同平台的渲染细节会有差异这个要有心理准备。2. 环境搭建从零到跑起第一个窗口2.1 Go 和 Node 的版本选择Wails v2 对 Go 版本有要求官方建议 Go 1.18 以上我实测用 Go 1.21 或 1.22 都很稳。Node 这边建议 18 LTS 或 20 LTS太新的版本偶尔会和某些构建工具打架。安装 Wails CLI 就一行go install github.com/wailsapp/wails/v2/cmd/wailslatest装完跑一下wails doctor这个命令会检查你的环境是否齐全包括 Go、Node、npm、以及平台相关的构建依赖Windows 上需要 WebView2 RuntimemacOS 需要 Xcode Command Line Tools。这一步千万别跳过我见过太多人卡在环境问题上结果发现是缺了 WebView2 运行时。2.2 创建项目时前端模板怎么选wails init -n ai-desktop -t vue-ts-t参数指定前端模板vue-ts就是 Vue3 TypeScript 的组合。Wails 内置了 vue、vue-ts、react、react-ts、svelte 等模板。我强烈建议用 TypeScript 版本因为 Wails 生成的绑定文件是带类型定义的配合 TS 能获得完整的类型提示调用 Go 方法时参数写错了编辑器直接报红这个体验提升非常大。项目结构大概是这样ai-desktop/ ├── app.go # 应用主逻辑 ├── main.go # 入口配置窗口选项 ├── wails.json # 项目配置 ├── go.mod └── frontend/ ├── src/ ├── wailsjs/ # 自动生成的绑定文件 ├── package.json └── vite.config.ts2.3 第一次 wails dev 容易踩的坑跑wails dev会启动一个开发服务器前端支持热重载Go 代码改动会自动重新编译。但有几个坑我踩过第一首次运行会下载依赖网络不好的话会很慢。Go 模块和 npm 包都要拉建议提前配好国内镜像源。第二前端热重载和 Go 重编译是两套机制。改 Vue 文件是秒级热更新改 Go 文件会触发整个应用重启窗口会闪一下这是正常的。第三Windows 上如果报 WebView2 相关的错去微软官网装一下 WebView2 Runtime 就行Win11 一般自带。第四wails dev默认会开一个浏览器调试端口你可以在浏览器里访问前端页面单独调试 UI这个对纯前端调整很方便。3. 把 AI 对话能力接进来Go 侧的核心设计3.1 为什么流式输出不能用普通返回值AI 对话的灵魂是流式输出——一个字一个字往外蹦用户能立刻看到响应。但前面说了Wails 的绑定方法返回值必须可序列化你没法直接返回一个持续推送数据的 channel。那怎么办Wails 提供了Runtime Events机制。Go 侧可以往事件总线发事件前端通过EventsOn监听。这就完美解决了流式推送的问题Go 每收到一段 AI 响应就runtime.EventsEmit推一次前端监听事件往界面上追加文本。这个设计的好处是解耦Go 只管发前端只管收中间不需要维护复杂的连接状态。3.2 一个可用的 AI 服务结构体长什么样我在app.go里定义了一个AIService结构体核心方法大概是这样组织的type AIService struct { ctx context.Context client *http.Client config *Config mu sync.Mutex cancelFn context.CancelFunc } func (a *AIService) SendMessage(sessionID string, content string) error { a.mu.Lock() ctx, cancel : context.WithCancel(a.ctx) a.cancelFn cancel a.mu.Unlock() go func() { defer cancel() // 构造请求发起流式 HTTP 调用 // 每收到一段内容就 EventsEmit runtime.EventsEmit(a.ctx, ai:chunk, ChunkPayload{ SessionID: sessionID, Content: piece, }) }() return nil }注意几个设计点用context管理生命周期用户点停止生成时调用cancelFn就能中断正在进行的请求这个在 AI 应用里是刚需。加锁保护 cancelFn因为可能同时有多个会话在跑或者用户快速点击不加锁会有竞态。异步执行SendMessage立刻返回真正的请求在 goroutine 里跑不阻塞前端调用。3.3 事件命名和数据结构的设计经验事件名我建议用模块:动作的格式比如ai:chunk、ai:done、ai:error。这样前端监听的时候一目了然也不容易和别的模块冲突。数据结构上每个 chunk 事件我都带上SessionID因为用户可能开了多个会话前端要根据 ID 把内容追加到对应的会话里。如果不带这个字段多会话场景下内容会串。还有一个细节流式结束要发一个明确的ai:done事件前端收到后才知道该把生成中的状态关掉。光靠 chunk 事件是判断不出结束的这个我一开始漏了导致界面上转圈图标一直不消失。4. Vue3 前端状态管理和流式渲染的实战4.1 用 Composition API 组织 AI 会话状态Vue3 的 Composition API 在处理这种复杂状态时比 Options API 清爽太多。我把会话相关的逻辑抽成了一个useChat组合式函数export function useChat() { const sessions refSession[]([]) const activeId refstring() const isGenerating ref(false) const activeSession computed(() sessions.value.find(s s.id activeId.value) ) function appendChunk(sessionId: string, content: string) { const session sessions.value.find(s s.id sessionId) if (session) { const lastMsg session.messages[session.messages.length - 1] lastMsg.content content } } return { sessions, activeId, activeSession, isGenerating, appendChunk } }这里有个性能上的坑如果消息列表很长每次 chunk 都触发整个列表重渲染界面会卡。我的做法是把正在生成的那条消息单独拎出来用shallowRef或者直接操作 DOM 文本节点等生成结束再合并回响应式数据。实测下来长对话场景下这个优化能明显减少卡顿。4.2 监听 Go 事件并驱动界面更新在组件挂载时注册事件监听import { EventsOn, EventsOff } from ../wailsjs/runtime/runtime onMounted(() { EventsOn(ai:chunk, (payload) { appendChunk(payload.SessionID, payload.Content) }) EventsOn(ai:done, (payload) { isGenerating.value false }) EventsOn(ai:error, (payload) { isGenerating.value false // 错误提示 }) }) onUnmounted(() { EventsOff(ai:chunk) EventsOff(ai:done) EventsOff(ai:error) })一定要在onUnmounted里注销监听否则组件销毁后事件还在会导致内存泄漏和重复处理。这个在单页应用里可能不明显但桌面应用长时间运行泄漏会累积。4.3 自动滚动到底部的正确姿势聊天界面最烦的就是新消息来了不自动滚到底。用scrollTop scrollHeight是最简单的但有个问题流式输出时每个 chunk 都滚一次如果用户手动往上翻看历史会被强行拽回底部体验很差。我的做法是加一个是否在底部的判断function isNearBottom(el: HTMLElement) { return el.scrollHeight - el.scrollTop - el.clientHeight 50 } function scrollToBottom(force false) { const el messageListRef.value if (!el) return if (force || isNearBottom(el)) { nextTick(() { el.scrollTop el.scrollHeight }) } }用户主动发消息时force true强制滚到底流式接收时只在用户本来就在底部才滚。这个小细节对体验的提升非常明显。5. 配置持久化与多会话管理5.1 配置文件放哪、用什么格式桌面应用的配置不能存在浏览器 localStorage 里因为 WebView 的存储可能被清理而且用户换机器就丢了。正确做法是存到系统的用户配置目录。Go 侧可以用os.UserConfigDir()拿到平台对应的配置目录func configPath() (string, error) { dir, err : os.UserConfigDir() if err ! nil { return , err } appDir : filepath.Join(dir, ai-desktop) os.MkdirAll(appDir, 0755) return filepath.Join(appDir, config.json), nil }Windows 上会落到%AppData%\ai-desktopmacOS 上是~/Library/Application Support/ai-desktopLinux 上是~/.config/ai-desktop。这个路径是系统推荐的符合各平台规范。格式我选 JSON因为 Go 和前端都能原生处理调试也方便。但要注意 API Key 这类敏感信息不能明文存。生产环境建议用系统密钥链或者至少做一层加密。我早期图省事直接明文存后来意识到这是个安全隐患改成了加密存储。5.2 多会话的数据结构设计会话数据我用一个数组存每个会话包含 id、标题、消息列表、创建时间type Session struct { ID string json:id Title string json:title Messages []Message json:messages CreatedAt time.Time json:createdAt } type Message struct { Role string json:role Content string json:content }会话标题我做了个自动生成用户发第一条消息时取前 20 个字符作为标题。这样侧边栏列表不会全是新对话找起来方便。这个细节看似小但实际用起来体验差别很大。5.3 保存时机别每次 chunk 都写磁盘流式输出时如果每来一个 chunk 就保存一次会话磁盘 IO 会非常频繁尤其是机械硬盘上会明显卡顿。我的策略是流式过程中只在内存里更新收到ai:done事件后才触发一次持久化用户切换会话、关闭窗口时也触发保存这样既保证了数据不丢又避免了频繁写盘。如果你担心程序崩溃丢数据可以加一个定时器比如每 30 秒兜底保存一次。6. 打包分发让应用真正能装到别人电脑上6.1 wails build 的关键参数开发完了要打包核心命令是wails build -platform windows/amd64 -clean-clean会清掉之前的构建缓存避免一些诡异的增量编译问题。-platform指定目标平台可以交叉编译但跨平台编译有时会遇到 CGO 相关的问题建议在目标平台上构建。打包产物在build/bin/目录下。Windows 上是一个.exemacOS 上是.appLinux 上是可执行文件。6.2 体积优化的几个实操点Wails 默认打包出来的体积已经不错了但还能再压编译时加-ldflags -s -w去掉符号表和调试信息能省几 MB。前端构建开启压缩Vite 默认生产构建就会压缩确认wails.json里的构建命令没问题。避免引入过重的前端依赖UI 框架选轻量的别为了一个组件引入整个库。我实测一个带完整聊天功能的 AI 应用Windows 打包出来大概 12MB 左右对比 Electron 动辄 80MB优势非常明显。6.3 分发时用户可能遇到的问题第一个是WebView2 运行时。Windows 10 早期版本可能没装用户第一次打开会报错。解决办法是在安装包里带上 WebView2 的引导安装程序或者用 Wails 的 NSIS 安装包模板它会自动处理。第二个是杀毒软件误报。自己编译的 exe 没有签名某些杀软会拦。这个只能通过代码签名证书解决个人项目的话在文档里说明一下让用户加白名单。第三个是macOS 的公证问题。macOS 上没签名的应用会被 Gatekeeper 拦用户需要右键打开。要正式分发得申请开发者证书做公证这个流程比较繁琐个人项目可以先在文档里说明。7. 几个我踩过的坑和对应的解法7.1 前端调用 Go 方法时的类型不匹配Wails 生成的绑定文件里Go 的int会映射成 JS 的numberstring映射成string但结构体字段名的大小写要注意。Go 结构体字段首字母大写才能被 JSON 序列化前端拿到的字段名就是大写的。我一开始前端用小写字段名去接结果全是 undefined排查了半天。经验Go 结构体字段用大写前端 TS 类型定义也用大写保持一致。7.2 窗口关闭时的资源清理用户点关闭按钮时如果有正在进行的 AI 请求需要优雅地取消掉否则可能留下僵尸 goroutine。Wails 提供了OnBeforeClose钩子func (a *App) OnBeforeClose(ctx context.Context) bool { a.aiService.CancelAll() a.saveAllSessions() return false // 返回 false 表示允许关闭 }在这里做清理保证数据保存和请求取消都完成后再退出。7.3 开发时热重载导致的状态丢失wails dev时改 Go 代码会重启应用前端状态全丢调试多会话逻辑时特别烦。我的应对是开发阶段把会话数据也持久化到磁盘重启后自动恢复这样调试体验好很多。虽然生产环境不一定需要这么频繁保存但开发时这个习惯能省不少时间。7.4 流式响应被缓冲的问题有一次我发现 AI 的响应不是流式的而是一股脑全出来。排查后发现是 HTTP 客户端那边的问题某些代理或中间层会缓冲响应。解决办法是在请求头里加Accept: text/event-stream并且确保读取响应体时用bufio.Scanner逐行读而不是io.ReadAll一次性读完。这个坑很隐蔽因为代码逻辑看起来没问题问题出在读取方式上。8. 这套方案还能往哪些方向扩展基础框架搭好之后能做的事情就多了。我目前想到和正在尝试的几个方向本地模型集成Go 侧可以调用本地推理服务的 HTTP 接口或者通过 CGO 绑定推理库。Wails 的架构对这块很友好因为 Go 处理进程调用和 IPC 都很成熟。多模态支持图片、文件的上传和处理Go 侧处理文件 IO 比前端有优势前端只负责展示。插件系统把不同 AI 服务商抽象成接口Go 侧做插件加载用户想接哪家就配哪家。这个用 Go 的 interface 设计起来很自然。系统级集成全局快捷键、托盘图标、开机自启这些 Wails 都有对应的 API做出来就是一个真正融入系统的桌面工具而不是一个套壳网页。我在实际项目里最深的一点体会是Wails 的价值不在于它某个功能特别强而在于它把 Go 的工程能力和 Web 前端的表现力用最低的胶水成本粘在了一起。你不用为了写桌面应用去学一门新语言也不用忍受 Electron 的臃肿。对于有 Go 基础的开发者来说这可能是目前做 AI 桌面应用性价比最高的路径。当然它也有自己的边界比如系统 WebView 的兼容性差异、生态相对年轻这些在选型时都要权衡。但就我个人的使用体验而言踩过的坑都在可解决范围内而它带来的开发效率和运行效率的提升是实打实能感受到的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑