news 2026/11/4 5:18:42

Wails v2 + Go + Vue3 打造轻量 AI 桌面应用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wails v2 + Go + Vue3 打造轻量 AI 桌面应用实战

1. 为什么我最终选了 Wails v2 而不是 Electron

做 AI 桌面应用这件事,我从去年就开始折腾了。最开始用的是 Electron,毕竟生态成熟、资料多,但真正打包发给朋友试用的时候,问题就来了:一个简单的对话工具,安装包 120MB 起步,冷启动要等三四秒,内存占用常年 300MB 往上。朋友第一句话就是"这玩意儿怎么这么卡"。后来我陆续试了 Tauri、Neutralino,最后落到 Wails v2 上,才算找到一个在体积、性能、开发体验三者之间比较平衡的方案。

Wails 的核心思路很直接:用 Go 写后端逻辑,用系统自带的 WebView 渲染前端界面,中间通过绑定机制做双向调用。它不像 Electron 那样把整个 Chromium 塞进安装包,而是复用操作系统已有的 WebView 组件——Windows 上用 WebView2,macOS 上用 WKWebView,Linux 上用 WebKitGTK。这个选择带来的直接好处就是安装包能压到 10MB 以内,内存占用通常只有 Electron 的三分之一左右。

那为什么前端选 Vue3 而不是 React 或者 Svelte?一方面是我自己 Vue 用得多,组合式 API 写起来顺手;另一方面 Vue3 的响应式系统和 Wails 的事件机制配合得很好,后端推数据过来直接改 ref 就能刷新界面,不需要额外搞状态管理。至于 AI 桌面应用这个场景,核心诉求其实就三个:能调本地模型或远程 API、能管理对话历史、能流式输出。这三件事用 Go 做后端都特别合适,尤其是流式输出,Go 的 goroutine 和 channel 处理起来比 Node 干净得多。

这篇文章我会把整个项目的搭建过程、关键决策、踩过的坑全部摊开讲。适合已经会一点 Go 和 Vue3、想做个真正能用的桌面工具的人。如果你完全没接触过这两个技术栈,建议先把 Go 的基础语法和 Vue3 的组合式 API 过一遍再回来,不然中间有些地方会卡住。

2. 环境准备与项目初始化

2.1 各平台依赖安装的差异

Wails 的安装本身不复杂,但不同系统下的前置依赖差别挺大,这一步没搞对后面编译会一直报错。我按平台分别说一下。

Windows 下需要先装 WebView2 Runtime。Win11 一般自带,Win10 可能需要手动装。然后装 Go(1.21 以上)、Node.js(18 以上),最后执行:

go install github.com/wailsapp/wails/v2/cmd/wails@latest

装完之后跑wails doctor,它会检查所有依赖是否齐全。这个命令一定要跑,它会明确告诉你缺什么。

macOS 下需要 Xcode 的命令行工具,执行xcode-select --install即可。Go 和 Node 用 Homebrew 装最省事。同样跑wails doctor验证。

Linux 下是最容易出问题的。需要装libgtk-3-dev、libwebkit2gtk-4.0-dev这两个包。Ubuntu/Debian 系:

sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev build-essential

这里有个坑:不同发行版的 WebKitGTK 包名不一样,Fedora 上叫webkit2gtk4.0-devel,Arch 上叫webkit2gtk。装错了wails doctor会提示找不到 webkit。另外 Linux 下编译出来的二进制依赖系统的 WebKitGTK 版本,如果目标机器版本不一致可能跑不起来,这个后面打包那节再细说。

2.2 创建项目骨架

环境齐了之后,创建项目一条命令:

wails init -n ai-desktop -t vue-ts

-t vue-ts表示用 Vue3 + TypeScript 模板。Wails 官方模板里 vue-ts 是我最推荐的,TypeScript 在前后端绑定的类型提示上帮助很大,尤其是后端方法多起来之后,没有类型提示很容易传错参数。

生成出来的目录结构大概是这样:

ai-desktop/ ├── build/ # 打包相关资源,图标、配置 ├── frontend/ # Vue3 前端 │ ├── src/ │ ├── package.json │ └── vite.config.ts ├── app.go # 应用主逻辑 ├── main.go # 入口 ├── wails.json # Wails 配置 └── go.mod

main.go里定义了窗口尺寸、标题、绑定的结构体。app.go是默认生成的示例结构体,我们后面会把它替换成真正的业务逻辑。

2.3 前端依赖与 Vite 配置

进到frontend目录,npm install装依赖。默认模板用的是 Vite,版本比较新。这里要注意一点:Wails 的前端开发服务器和普通 Vite 项目略有不同,vite.config.ts里已经配好了端口和代理,不要随便改端口,否则 Wails 找不到前端。

如果你要用 SCSS,装一下sass就行:

npm install -D sass

Vue3 项目里用 SCSS 直接在<style lang="scss">里写就行,不需要额外配置。但如果你用了@use语法引入变量文件,注意路径别名要在vite.config.ts里配好,不然编译会报找不到文件。

3. 前后端绑定机制:Wails 的核心

3.1 绑定到底是怎么工作的

Wails 最核心的机制就是"绑定"。你在 Go 里定义一个结构体,把它的实例传给Bind,Wails 会自动生成对应的 TypeScript 类型定义和调用代码,前端就能像调本地函数一样调 Go 方法。

举个例子,我在app.go里定义:

type App struct { ctx context.Context } func (a *App) Greet(name string) string { return "Hello " + name }

然后在main.go里:

err := wails.Run(&options.App{ Title: "AI Desktop", Width: 1024, Height: 768, Bind: []interface{}{ app, }, })

编译一次之后,frontend/wailsjs/go/目录下会自动生成App.d.ts和App.js,前端直接:

import { Greet } from '../wailsjs/go/main/App' const result = await Greet('World')

这个机制的妙处在于:你不需要手写任何 HTTP 接口、不需要定义路由、不需要处理跨域。前后端就像在同一个进程里调用函数一样。底层其实是 Wails 通过 WebView 的 bridge 做了一层消息传递,但开发者完全感知不到。

3.2 方法签名的约束

绑定不是万能的,方法签名有约束。参数和返回值必须是可 JSON 序列化的类型。基本类型、结构体、切片、map 都没问题,但 channel、函数、复杂指针这些不行。

返回值方面,Wails 支持两种形式:单返回值,或者(结果, error)双返回值。后者在前端会变成 Promise 的 reject,用 try/catch 接。我强烈建议所有可能出错的方法都用双返回值,比如调用 AI API 的方法,网络错误、超时、返回格式错误都要能传到前端。

func (a *App) Chat(prompt string) (string, error) { resp, err := callAI(prompt) if err != nil { return "", err } return resp, nil }

前端:

try { const reply = await Chat(prompt) } catch (e) { console.error('调用失败', e) }

3.3 事件机制:后端主动推数据

绑定解决的是前端调后端,那后端怎么主动推数据给前端?答案是事件。Wails 提供了runtime.EventsEmit,后端可以随时发事件,前端用EventsOn监听。

这个机制对流式输出特别关键。AI 返回内容是逐字逐句来的,不可能等全部生成完再一次性返回。我的做法是:前端调一个StartChat方法启动对话,后端在 goroutine 里不断读取流式响应,每读到一段就EventsEmit一个事件,前端监听这个事件往界面上追加文字。

func (a *App) StartChat(prompt string) { go func() { stream := callAIStream(prompt) for chunk := range stream { runtime.EventsEmit(a.ctx, "chat:chunk", chunk) } runtime.EventsEmit(a.ctx, "chat:done", nil) }() }

前端:

import { EventsOn } from '../wailsjs/runtime/runtime' EventsOn('chat:chunk', (chunk: string) => { messageContent.value += chunk })

注意a.ctx是 Wails 在启动时通过OnStartup回调传进来的 context,事件发送必须用它,否则事件发不出去。这个 ctx 一定要在结构体里存好,我一开始忘了存,事件一直没反应,排查了半天。

4. AI 对话核心功能的实现

4.1 流式输出的完整链路

流式输出是整个应用体验的关键。用户输入问题后,如果等三五秒才看到全部回答,会觉得很卡;但如果文字是一个一个蹦出来的,即使总时间一样,体感也流畅得多。

实现链路是这样的:前端输入框回车 -> 调用 Go 的StartChat-> Go 发起 HTTP 请求到 AI 服务 -> 读取 SSE 流 -> 每读到一个 chunk 就发事件 -> 前端监听事件追加文字。

Go 这边读取 SSE 流,用标准库的bufio.Scanner就够了:

resp, err := http.Post(url, "application/json", body) if err != nil { return } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "data: ") { data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { break } // 解析 JSON,提取内容 var chunk Chunk json.Unmarshal([]byte(data), &chunk) runtime.EventsEmit(a.ctx, "chat:chunk", chunk.Content) } }

这里有个细节:SSE 的每一行可能不完整,尤其是网络抖动的时候。稳妥的做法是用bufio.Reader配合ReadString('\n'),或者自己维护一个缓冲区。我实测下来Scanner在大多数情况下够用,但如果你的应用对稳定性要求高,建议用Reader。

4.2 对话历史的管理

对话历史不能只存在前端内存里,关掉应用就没了。我用 SQLite 存,Go 这边用modernc.org/sqlite这个纯 Go 实现的驱动,不需要 CGO,交叉编译的时候省心很多。

表结构很简单:

CREATE TABLE conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER, role TEXT, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );

每次对话开始时创建一个 conversation,每条消息存一行。前端加载历史的时候,调一个GetConversations拿列表,点进去再调GetMessages拿具体消息。

这里有个经验:消息内容可能很长,尤其是 AI 的长回答。SQLite 存 TEXT 没问题,但前端渲染的时候要注意性能。如果一条消息几千字,直接v-html渲染会卡。我的做法是分段渲染,或者用虚拟滚动。不过对于普通对话场景,几千字其实还好,真正卡的是历史消息特别多的时候,这时候要做分页加载。

4.3 多轮对话的上下文拼接

AI 本身是无状态的,多轮对话靠的是每次把历史消息一起发过去。这里要注意 token 限制,不能无限拼接。我的策略是保留最近 N 轮对话,N 根据模型上下文窗口动态调整。

func buildContext(history []Message, maxTokens int) []Message { var result []Message total := 0 // 从最新往旧遍历 for i := len(history) - 1; i >= 0; i-- { msg := history[i] tokens := estimateTokens(msg.Content) if total+tokens > maxTokens { break } result = append([]Message{msg}, result...) total += tokens } return result }

estimateTokens是个粗略估算,中文大概一个字一个 token,英文大概四个字符一个 token。精确计算需要 tokenizer,但为了简单,估算够用了。真正要精确的话,可以引入 tiktoken 的 Go 实现,但会增加依赖体积。

5. 界面设计与交互细节

5.1 布局结构

界面我用了最经典的三栏布局:左侧会话列表,中间对话区,右侧可选的设置面板。Vue3 里用 flex 布局就能搞定,不需要引入 UI 框架。说实话,AI 对话应用的界面不复杂,引入 Element Plus 或者 Ant Design Vue 反而会让打包体积变大,而且样式定制起来麻烦。

左侧会话列表用v-for渲染,每项显示标题和最后一条消息的摘要。中间对话区用v-for渲染消息气泡,用户消息靠右,AI 消息靠左。底部是输入框,支持回车发送、Shift+回车换行。

5.2 流式输出的滚动处理

流式输出时有个体验问题:文字不断追加,如果用户没手动滚动,应该自动滚到底部;但如果用户正在往上翻看历史,就不应该强制拉回底部。这个逻辑要判断:

const scrollToBottom = () => { const el = chatContainer.value if (!el) return const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100 if (isNearBottom) { el.scrollTop = el.scrollHeight } }

每次收到 chunk 就调一次。100这个阈值是我试出来的,太小了用户稍微滚一点就被拉回去,太大了用户滚很远还在自动滚。你可以根据自己习惯调整。

5.3 代码块高亮

AI 回答里经常有代码,直接显示纯文本很难看。我用highlight.js做高亮,配合marked解析 Markdown。这里有个坑:流式输出的时候,Markdown 可能是不完整的,比如代码块的 ``` 只出现了一半,这时候解析会出错。我的做法是流式过程中先不解析,等chat:done事件来了再整体渲染一次 Markdown。流式过程中就显示纯文本,虽然中间过程没有高亮,但至少不会报错。

EventsOn('chat:done', () => { const lastMsg = messages.value[messages.value.length - 1] lastMsg.html = marked.parse(lastMsg.content) lastMsg.streaming = false })

5.4 深色模式

深色模式现在基本是标配了。我用 CSS 变量做主题切换,定义两套变量,切换的时候改根元素的 class 就行。

:root { --bg-primary: #ffffff; --text-primary: #1a1a1a; } .dark { --bg-primary: #1e1e1e; --text-primary: #e0e0e0; }

Wails 提供了WindowSetDarkTheme之类的 API,但那是控制窗口标题栏的,页面内容还是要自己管。我建议跟随系统主题,用window.matchMedia('(prefers-color-scheme: dark)')检测,同时提供手动切换。

6. 打包与分发

6.1 各平台打包命令

Wails 打包一条命令:

wails build

默认会生成当前平台的安装包。Windows 下生成.exe,macOS 下生成.app,Linux 下生成可执行文件。

如果要指定平台,用-platform参数:

wails build -platform windows/amd64 wails build -platform darwin/universal wails build -platform linux/amd64

darwin/universal会同时打包 Intel 和 Apple Silicon 两个架构,生成一个通用二进制。这个很实用,不然用户还得自己选版本。

6.2 体积优化的几个手段

默认打包出来的体积其实已经很小了,但还能再压。几个手段:

第一,编译时加-ldflags "-s -w",去掉符号表和调试信息,能省几 MB。Wails 的wails.json里可以配:

{ "build:ldflags": "-s -w" }

第二,前端构建产物用 gzip 压缩。Wails 支持把前端资源嵌入二进制,但默认不压缩。可以在wails.json里开启。

第三,如果用了 SQLite,确保用的是纯 Go 驱动,避免 CGO 带来的额外依赖。

我实测下来,一个带 SQLite、Markdown 渲染、代码高亮的 AI 对话应用,Windows 安装包大概 8MB 左右,macOS 的.app大概 12MB。对比 Electron 动辄上百 MB,这个体积已经很能打了。

6.3 Linux 分发的坑

Linux 下分发是最麻烦的。Wails 编译出来的二进制依赖系统的 WebKitGTK,如果目标机器没装或者版本不对,直接跑不起来。

我的做法是打包成 AppImage,把依赖一起打进去。用linuxdeploy工具配合 Wails 的产物,能生成一个自包含的可执行文件。不过 AppImage 的构建流程比较繁琐,需要写脚本。另一个选择是提供.deb包,在control文件里声明依赖libwebkit2gtk-4.0-37,让包管理器去处理。

如果你只是自己用或者小范围分发,直接给二进制文件加个说明"需要安装 libwebkit2gtk"也行。但如果要面向普通用户,还是老老实实做 AppImage 或者 deb 包。

7. 常见问题与排查实录

7.1 编译报错找不到 webkit

这是 Linux 下最常见的问题。wails doctor会提示找不到 webkit2gtk。原因通常是包名不对或者版本不对。Ubuntu 22.04 之后,libwebkit2gtk-4.0-dev可能被libwebkit2gtk-4.1-dev替代了。Wails v2 目前主要支持 4.0,如果系统只有 4.1,需要装兼容包或者降级。

7.2 前端改了代码不生效

Wails 开发模式下,前端是热更新的,但有时候改了wails.json或者 Go 代码,需要重启wails dev。如果前端改了没反应,先检查是不是 Vite 的 HMR 挂了,重启一下开发服务器。另外,wailsjs目录下的绑定文件是自动生成的,不要手动改,改了下次编译会被覆盖。

7.3 事件收不到

前面提过,EventsEmit的 ctx 必须是OnStartup传进来的那个。如果事件发出去前端收不到,先检查 ctx 是不是 nil,或者是不是用了context.Background()。另外,前端EventsOn注册的时机要在组件挂载之后,如果在setup顶层直接注册,可能组件还没挂载就注册了,虽然一般没问题,但稳妥起见放在onMounted里。

7.4 打包后白屏

打包后白屏通常是前端资源路径问题。Wails 把前端资源嵌入二进制,路径是相对的。如果vite.config.ts里base配成了绝对路径,打包后就会找不到资源。确保base是'./'或者不配。

7.5 常见问题速查表

问题现象可能原因解决方法
编译报错找不到 webkitLinux 缺依赖或版本不对装 libwebkit2gtk-4.0-dev
前端改动不生效HMR 挂了重启 wails dev
事件收不到ctx 为 nil 或未存在 OnStartup 里存 ctx
打包后白屏资源路径错误vite base 设为 './'
流式输出卡顿事件发送太频繁加节流,每 50ms 发一次
SQLite 报错CGO 未开启或驱动不对用 modernc.org/sqlite

8. 我踩过的几个印象深刻的坑

第一个坑是事件发送频率。一开始我每收到一个字符就发一次事件,结果前端渲染压力巨大,界面直接卡死。后来改成攒够一定长度或者每隔 50ms 发一次,流畅度立刻上来了。这个节流逻辑在 Go 这边做,用一个 timer 控制。

第二个坑是 SQLite 的并发写。Go 的 goroutine 同时写数据库会报database is locked。解决方法是设置db.SetMaxOpenConns(1),或者用 WAL 模式。我用的是前者,简单粗暴但有效,因为桌面应用的写入量本来就不大。

第三个坑是 macOS 的签名。打包出来的.app如果没签名,用户第一次打开会被系统拦截,提示"无法验证开发者"。自己用的话右键打开就行,但要分发给别人,还是得搞个开发者证书签名。这个流程比较繁琐,个人项目如果只是小范围分享,可以在说明里写清楚怎么绕过。

第四个坑是窗口尺寸的记忆。用户调整了窗口大小,下次打开应该保持。Wails 本身不提供这个功能,需要自己存。我在OnShutdown里把窗口尺寸存到配置文件,OnStartup里读出来设置。配置文件用 JSON 存在用户目录下就行。

9. 后续可以扩展的方向

这个应用目前只做了基础的对话功能,但 Wails + Go 的组合能做的事情远不止这些。比如可以接入本地模型,用 Go 调用 llama.cpp 的绑定,实现完全离线的 AI 对话。也可以做多模型切换,在设置里配置不同的 API 端点,前端一个下拉框切换。

再比如可以做对话导出,把历史对话导出成 Markdown 或者 PDF。Go 这边生成 PDF 可以用gofpdf,Markdown 直接拼字符串就行。还可以做提示词模板管理,把常用的提示词存起来,一键填充。

工具调用也是个有意思的方向。让 AI 能调用本地的一些工具,比如读文件、执行命令、查天气。Go 这边定义好工具函数,AI 返回工具调用请求时,后端执行完再把结果喂回去。这个链路稍微复杂一点,但 Wails 的事件机制完全能支撑。

我个人觉得,Wails 这个方案最适合的就是这种"本地能力 + 网络能力"混合的桌面工具。纯 Web 能做的事情没必要做成桌面应用,但一旦涉及到本地文件、本地数据库、本地模型,Wails 的优势就体现出来了。体积小、启动快、内存占用低,这三点对于桌面应用来说太重要了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 5:18:39

Archery SQL审核平台:部署、工单与回滚实践

简介&#xff1a;《archery使用手册》面向研发工程师、测试人员与数据库管理员&#xff0c;系统讲解这款SQL审核查询平台的落地用法&#xff0c;帮助团队把SQL上线、优化与实例运维纳入规范化流程。压缩包共1个doc文件&#xff0c;约1.2MB&#xff0c;以图文结合的方式组织内容…

作者头像 李华
网站建设 2026/11/4 5:18:20

AI驱动的电商推荐系统:架构、优化与实践

1. 项目概述Facebook Marketplace近期推出了一系列由人工智能驱动的新功能&#xff0c;这标志着社交电商平台正在向智能化方向迈进。作为一个长期关注电商领域的技术从业者&#xff0c;我注意到这些更新不仅仅是简单的功能叠加&#xff0c;而是基于深度用户行为分析和机器学习算…

作者头像 李华
网站建设 2026/11/4 5:18:17

金融服务系统实战:账户、风控、交易与对账的完整落地指南

这几年我前前后后做过好几个跟金融服务沾边的项目&#xff0c;从账户体系、交易链路到清结算对账都摸过一遍。说白了&#xff0c;所谓 financial-services&#xff0c;就是把钱、信用、风险、数据这四件事用系统化的方式组织起来&#xff0c;让用户能安全地存钱、花钱、借钱、投…

作者头像 李华
网站建设 2026/11/4 5:16:38

数据仓库DWD层构建:从清洗到装载的实战解析

1. 项目背景与核心价值数据仓库DWD层作为数据仓库体系中的核心环节&#xff0c;承担着对原始数据进行清洗、转换和轻度汇总的关键任务。尚硅谷这套数仓搭建方案中的DWD层数据装载脚本&#xff0c;实际上解决的是企业级数据仓库实施过程中最关键的"数据管道"问题。我在…

作者头像 李华
网站建设 2026/11/4 5:15:46

HIDA 2026健康信息化与数据分析国际会议投稿指南

1. 会议背景与核心价值第二届健康信息化与数据分析国际学术会议&#xff08;HIDA 2026&#xff09;由南京大学主办&#xff0c;是健康信息技术与医疗数据分析领域的重要学术交流平台。作为ACM出版社支持的系列会议&#xff0c;其往届会议论文已被EI Compendex和Scopus稳定检索&…

作者头像 李华
网站建设 2026/11/4 5:15:05

工业数据采集网关SSF-BOX-200功能与应用解析

1. 工业数据采集网关SSF-BOX-200深度解析在工业4.0和智能制造的大背景下&#xff0c;生产设备的互联互通已成为企业数字化转型的基础需求。SSF-BOX-200作为一款工业级PLC数据采集网关&#xff0c;正是为解决这一需求而设计的专业设备。我在工业自动化领域工作多年&#xff0c;参…

作者头像 李华