ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

全栈实战:用 Tauri + Go + Vue 3 自造桌面仪表盘 Status Deck

全栈实战:用 Tauri + Go + Vue 3 自造桌面仪表盘 Status Deck 说实话做开发这些年我自己最烦的一种状态就是工作台上一堆窗口开着终端里跑着服务、浏览器开着监控面板、任务栏堆着十几个标签页可我还是说不清楚此刻机器到底忙不忙、接口到底还活着没有。所以我决定动手做一个自己的 Status Deck——简单说就是一个给开发者用的桌面仪表盘把系统资源、服务探活、任务状态这些高频信息统一放到一块独立的桌面上让我不用切窗口、不用开浏览器抬眼就能扫完当前环境的状态。这个项目我定位成“全栈自造”意思是前端、后端、桌面壳、数据存储全部自己搭不依赖现成的监控平台。目前第一篇先聊聊整体思路、技术选型和第一个可运行版本的落地过程后面还会继续拆细模块的实现。如果你也在做全栈练手项目或者想给自己攒一套顺手的小工具这篇应该能给你一个可以照着搭的骨架。1. 为什么自造一个桌面仪表盘而不是继续用现成工具1.1 开发者的信息工作流缺的不是工具而是聚合先说说我的真实使用场景。白天写代码的时候我要同时确认几类信息本地起的后端服务是否正常、数据库连接有没有断、CPU 和内存是不是已经告急、当前的 Git 分支和待办任务到了哪一步。这些东西本身都有对应的工具终端、浏览器标签页、IDE 插件都能看但它们彼此隔离信息是碎片的。碎片化的结果是检查状态这件事变成了一个个小的上下文切换。每切一次窗口注意力就被打断一次遇到调接口调一半还要去看服务挂没挂的情况真的很影响节奏。我想要的是把高频状态聚合到一个地方让它像一块“仪表盘”一样常驻桌面而不是藏在某个应用的下拉菜单里。Status Deck 的名字也是这么来的。Deck 在英文里有“甲板”“卡组”的意思我理解成一块承载信息的平面所有状态卡片都铺在上面。它的核心不是做一个功能强大的监控系统而是做一个符合我个人习惯的“状态入口”打开电脑就能看到的那一种。1.2 这个项目要解决什么问题适合谁来复刻这个项目要解决三个具体问题第一系统资源信息随手可见不用每次开任务管理器第二本地服务是否存活一目了然避免“接口超时了才发现服务挂了”的尴尬第三所有配置都可以自己改想加一个监控项就加一个不被现成工具的固定字段限制。它适合三类人正在从“页面仔”往前端转全栈的同学可以通过这个项目把 Vue 前端、Go 后端、桌面端打包融会贯通想给自己做效率工具但不知道从哪下手的开发者可以参考这套轻量架构做其他主题的桌面工具还有就是对数据敏感、不愿意什么都往云端放的人所有数据都在本地断电断网都不影响。当然它不适合当成生产级监控平台去用。这个项目的目标是轻、快、可控任何会引入重依赖的方案我都会回避。这样换来的是启动速度快、加点功能不费劲、整个代码库自己都清楚。2. 技术选型从桌面壳到后端的完整组合2.1 桌面壳为什么选 Tauri 而不是 Electron桌面仪表盘有两种常见技术路线Electron 和 Tauri。Electron 生态成熟、资料多用 Node 写主进程打包出来体积通常上百 MB运行时内存占用也比较可观。Tauri 是近几年发展很快的替代方案它用系统的 WebView 渲染前端主逻辑用 Rust 编写打包体积小很多内存占用也更友好。我最后选了 Tauri 2核心原因是这个项目是常驻型桌面工具需要开机自启、后台运行、低资源占用。如果天天开着一个占用四五百 MB 内存的 Electron 壳那跟多开一个浏览器标签页也没什么区别完全违背了做仪表盘的初衷。Tauri 的前端依然可以用 Vue 3 和 Vite 开发学习成本几乎平移Rust 那部分目前只需要配置基本能力不太需要深入写业务逻辑。提示如果你对 Rust 实在陌生也可以先用 Electron 跑通架构后续再迁 Tauri因为前端代码和通信协议是可以共用的。我自己是先写了一个最小 Tauri 窗口确认能加载前端页面再往后端走这一步只要成功后面基本不会有大坑。2.2 后端与存储Go 单二进制加纯 Go 驱动的 SQLite后端我选了 Go没有用 Node 也没有用 Python理由很直接Go 编译出来是一个单文件二进制部署和启动特别简单而且并发处理能力足够支撑这种本地小规模服务。Status Deck 的后端要做的事情是定时采集系统指标、执行探活任务、提供 HTTP 接口给前端这些正好是 Go 的标准强项。存储选 SQLite而且是纯 Go 驱动实现的那个版本。它不需要额外安装数据库服务数据文件就是一个本地文件非常适合桌面应用。之所以强调“纯 Go 驱动”是因为标准库里的 SQLite 驱动依赖 CGO跨平台编译时要额外处理 C 编译器GOOSwindows 打包的时候特别容易翻车后面踩坑部分我会专门展开。前端骨架用 Vue 3 Vite TypeScript组件样式用的是中性极简风格尽量不给仪表盘增加视觉噪音。可视化部分我没上重型图表库因为仪表盘核心是状态数字不是趋势分析等到后面需要画折线趋势时再引 ECharts 也不迟。技术栈汇总如下表层次技术方案选择理由桌面壳Tauri 2包体小、内存占用低、支持开机自启前端Vue 3 Vite TS开发体验好、组件化清晰、生态成熟后端Go net/http单二进制、并发稳定、启动快存储SQLitemodernc 纯 Go 驱动零依赖、本地文件、免部署采集gopsutil跨平台获取系统指标的标准库3. 整体架构一条从系统指标到桌面卡片的数据流水线3.1 模块划分与进程组织整个项目按进程来分其实就两个Tauri 桌面壳进程和 Go 后端进程。Tauri 进程负责创建窗口、加载前端资源并提供系统级能力比如开机自启、窗口置顶。Go 后端进程是真正的业务主力采集、探活、数据存储都由它完成。我特意没有把采集逻辑写进前端也没有直接用 Tauri 的 IPC 通道去调系统命令。原因是分层更清楚前端只负责展示后端只负责数据和状态互相通过 HTTP 接口通信。这样即便以后不用 Tauri想换成普通网页或者做多端入口前端代码也能直接用后端的复用价值就高了。Tauri 启动时会在 Rust 的 setup 钩子里拉起 Go 二进制退出时杀掉子进程。开发环境下Go 后端用go run单独启动正式环境里它被打包成statusd可执行文件放在应用资源目录下。前端页面统一访问http://127.0.0.1:17890后端在本地监听这个端口不暴露到局域网。3.2 数据流采集、存储、读取三层分离数据流我设计成了三层彼此之间不直接依赖。用户视角上它表现为“采集器写数据、API 读数据、前端刷页面”的清爽链条代码结构上它也方便我单独测每一层。采集层是一组由 ticker 触发的任务比如每 10 秒采集一次 CPU 和内存写入 metrics 表探活任务按各自配置的间隔执行结果写入 probe_results 表。存储层只做两件事写入快照、按时间范围读取历史记录。API 层提供两个主要接口/api/overview返回当前仪表盘需要展示的聚合状态/api/history返回指定时间窗口里的指标记录。前端不直接访问数据库避免数据并发写读互相干扰也保住了安全边界。这里有一个很关键的设计取舍数据在采集之后先写库然后 API 再查询输出而不是写库的同时直接推给前端。多一个存储步骤看似绕了弯但好处是前端每次刷新看到的是一致的数据快照而且后端重启后历史状态还在不是重启即失忆。数据库建表也很简单核心就两张表CREATE TABLE IF NOT EXISTS metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, node TEXT NOT NULL, payload TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS probe_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, probe_name TEXT NOT NULL, ok INTEGER NOT NULL, latency_ms INTEGER NOT NULL DEFAULT 0, error_msg TEXT, checked_at DATETIME DEFAULT CURRENT_TIMESTAMP );4. 核心功能实现先跑起来的是这四个模块4.1 配置中心用 TOML 管理一切可调参数第一个落地模块是配置中心。我用了 TOML 格式因为它比 JSON 更适合写给人看的配置支持注释缩进要求也宽松。config 目录下的deck.toml是全局配置里面包含后端监听地址、采集间隔、探活任务列表。我当时把探活任务设计成数组结构每个探活项都有独立的名字、类型、地址和超时参数。这样做的好处是想加一个新的监控对象时不用改代码加一段配置就行。下面是一个简化版的配置文件[server] listen 127.0.0.1:17890 [collector] interval_sec 10 [[probe]] name 本地API type http url http://127.0.0.1:17890/health interval_sec 30 timeout_ms 5000 expect_status 200 [[probe]] name 数据库端口 type tcp host 127.0.0.1 port 3306 interval_sec 60 timeout_ms 3000注意配置文件的路径在后端里不能写死。开发的时候可能是项目目录下的 config/deck.toml打包之后就要去取应用数据目录否则正式环境找不到配置会直接 panic。我的做法是做一个配置加载函数优先读环境变量指定的路径没有就用默认的本地目录。4.2 系统资源采集CPU 与内存的一线数据系统指标采集用的是 gopsutil 这个库它对跨平台支持很好Windows、macOS、Linux 都能拿到统一结构的数据。CPU 使用率这里有个细节cpu.Percent默认返回的是一个时间区间内的平均值单次调用拿到的经常是 0必须传入一个间隔参数让它在内部做采样。我设置的是 500 毫秒采样周期基本能反映当前负载。内存这块比较简单mem.VirtualMemory()返回的UsedPercent就是整体使用率Total是物理内存总量。采集完成后组装成一个 Snapshot 结构再转成 JSON 写入数据库。核心代码大概是这样type Snapshot struct { CPUUsedPercent float64 json:cpu MemUsedPercent float64 json:mem MemTotal uint64 json:mem_total TakenAt time.Time json:taken_at } func Collect() (*Snapshot, error) { pct, err : cpu.Percent(500*time.Millisecond, false) if err ! nil { return nil, err } vm, err : mem.VirtualMemory() if err ! nil { return nil, err } return Snapshot{ CPUUsedPercent: roundTo(pct[0], 2), MemUsedPercent: roundTo(vm.UsedPercent, 2), MemTotal: vm.Total, TakenAt: time.Now(), }, nil }要注意cpu.Percent的结果是切片在false模式下只有一个元素。如果你的机器核心数很多还想要每个核心的使用率曲线可以把参数改成true那么返回的就会是每个核心的独立使用情况。4.3 探活任务HTTP 与端口检查怎么做探活是 Status Deck 最实用的模块之一。对于 HTTP 类型的目标我发一个HEAD请求拿到状态码就判断目标是否健康对于 TCP 类型的目标用带超时的拨号连接去试探端口。探活结果不只记成功还是失败还会记录延迟这样仪表盘上既能显示红绿状态也能显示响应速度的变化趋势。调度逻辑我用了简单的定时器管理每个探活任务注册自己的 ticker触发时在 goroutine 里执行结果通过 channel 汇入结果处理器。这样不会因为某一个探活目标超时而阻塞其他任务。超时处理非常关键HTTP 请求如果不设置超时时间DNS 解析卡住时整个任务会挂很久所以我都会用http.Client.Timeout强制设置 5 秒上限。关键代码片段func probeHTTP(name, url string, timeout time.Duration, expect int) ProbeResult { client : http.Client{Timeout: timeout} start : time.Now() resp, err : client.Head(url) latency : time.Since(start).Milliseconds() if err ! nil { return ProbeResult{Name: name, OK: false, Latency: latency, Error: err.Error()} } defer resp.Body.Close() return ProbeResult{ Name: name, OK: resp.StatusCode expect, Latency: latency, Error: fmt.Sprintf(status%d, resp.StatusCode), } }这里还有一个容易忽略的点网络不可用或者目标地址无法解析时很多探活工具会直接报失败但这类失败不一定是服务真的挂了也可能是本机网络波动。因此判断阈值我会设成“连续失败 3 次才标记异常”避免单次抖动引发误报。4.4 前端仪表盘卡片化布局与数据刷新前端部分是一个标准的 Vue 3 单页应用。整体布局就是栅格式卡片每张卡片承担一类状态展示系统资源卡显示 CPU 和内存的使用率探活状态卡枚举所有探活目标并显示红绿状态底部留一个事件流区域展示最近的历史记录。为了让界面更像仪表盘而不是普通后台管理页我给每张卡片设置了居中的大字数值状态改变时通过颜色过渡反馈。数据刷新用的是最简单也最稳定的方案兜底轮询。进入页面后先拉一次/api/overview之后每隔 5 秒拉一次。import { onMounted, onBeforeUnmount, ref } from vue; const overview refOverview | null(null); let timer: ReturnTypetypeof setInterval; async function refresh() { const res await fetch(http://127.0.0.1:17890/api/overview); overview.value await res.json(); } onMounted(() { refresh(); timer setInterval(refresh, 5000); }); onBeforeUnmount(() clearInterval(timer));5 秒间隔是在视觉实时性和资源开销之间折中的结果。太短会频繁触发前端渲染太长会让人觉得仪表盘不够“活”。如果以后想让状态推送更即时可以把轮询改成 WebSocket 或者 SSE但第一版我还是建议先跑通轮询因为它的出错面最小排查起来也最容易。5. 踩坑实录本地桌面工具最容易翻车的六个地方5.1 跨平台路径与采集单位的差异我第一版代码是在 macOS 上写的放到 Windows 上跑的时候立刻遇到了两个问题。第一个是路径分隔符硬编码/的配置路径在 Windows 上解析失败第二个是磁盘使用率采集gopsutil 在 Windows 上对每个盘符返回独立的挂载点如果不对盘符做汇总仪表盘上就会莫名出现“C 盘已用 xx%”的碎片信息而不是整机汇总。解决办法是统一用filepath.Join处理路径绝不手写路径拼接磁盘使用率部分则按挂载点聚合把多个分区综合成一个整体指标。类似这种跨平台差异靠读文档是发现不完的最快的办法就是在目标平台上跑一遍最小用例。5.2 SQLite 的 CGO 问题与纯 Go 驱动替代这是我在打包阶段被卡最久的一个坑。网上大量 SQLite 教程推荐的是github.com/mattn/go-sqlite3它天然带 CGO 依赖本地开发没问题一旦交叉编译 Windows 版本就会蹦出 CGO 编译错误的报错要么得装 MinGW要么得写复杂的构建脚本。后来我换成了modernc.org/sqlite这是纯 Go 实现的 SQLite 驱动接口完全兼容database/sql代码几乎不用改交叉编译直接用常规的 GOOS/GOARCH 就行。对于这种桌面小工具来说减小编译链路的复杂度比多花一点运行时内存更有价值我建议有交叉编译需求的读者直接选纯 Go 驱动。5.3 轮询错峰不要让前端和后端互相打架前端每 5 秒拉一次数据后端每 10 秒写一次采集快照如果两边的时钟完全对齐很容易出现一种现象前端请求恰好撞上后端写库的瞬间SQLite 的写锁和读请求排队接口响应速度偶发性变慢。解决思路并不是上复杂的缓存而是让两边错峰。我的做法是后端采集周期的相位设置成“启动后延迟 3 秒再开始”前端的轮询启动时先立即拉一次之后用固定间隔。这样实际操作下来两个周期不会长期处于同一相位对撞的状态接口响应也稳定多了。5.4 日志与数据文件的生命周期管理Status Deck 会在本地持续运行如果日志和数据文件不做清理时间长了 data 目录能涨到几个 GB。我第一次跑了一周发现数据库已经积累了十万条历史指标明明仪表盘只需要最近几分钟的数据留这么多历史数据纯属浪费。后来我在后端加了两层治理第一层是表数据清理任务每天凌晨执行一次只保留最近 7 天的记录第二层是日志滚动超过 10MB 就把旧日志改成.bak再新建当天日志。对于工具类项目生命周期的治理往往比功能本身更影响体验毕竟没人希望一个仪表盘反过来把自己磁盘吃满。5.5 探活误报把单次失败看成一次抖动一开始我把探活失败直接标红结果网络偶尔波动一下就满屏告警后来我意识到单次失败的价值很有限连续失败才能说明问题。于是我在探活结果处理器里加了一个状态机逻辑每个探活目标维护连续失败计数达到 3 次才切换为异常状态恢复时则要求连续成功 1 次即可。这个阈值可以根据服务重要程度调整。重要服务可以把失败阈值降到 2 次提示更灵敏非关键服务可以放宽到 5 次减少打扰。这个经验是纯粹从天天被误报烦到之后总结出来的文档里通常不会写。5.6 Tauri 侧的健康检查窗口关掉不等于应用退出用 Tauri 还有一个容易忽略的坑用户点掉窗口后进程默认是退出的这会导致后端子进程失去父进程变成孤儿进程继续驻留。如果不处理下次再启动应用时会发现端口已被占用起不来。我的处理方式是启用 Tauri 的系统托盘关闭窗口时只隐藏窗口真正退出时通过托盘菜单触发退出流程后端子进程一起杀掉。开机自启则通过 Tauri 的 autostart 插件完成。这套逻辑听起来简单但涉及桌面应用“生命周期”的管理值得单独列一个模块来看待。6. 项目目录、系列计划与我的个人体会6.1 可运行版本的目录长什么样目前这个可运行版本的目录结构非常简单五个主要目录status-deck/ ├─ frontend/ # Vue3 Vite TS 前端 ├─ backend/ # Go 子进程采集与探活 ├─ src-tauri/ # Tauri 桌面壳 ├─ config/ # deck.toml 配置文件 └─ data/ # 数据库文件与日志写代码的时候我刻意让 backend 保持“可独立运行”的状态哪怕没有桌面壳单独跑 Go 进程也可以直接通过 curl 访问 API 验证数据。这个习惯帮我排除了很多问题界面不出数据先 curl 后端接口能通说明问题在前端不通那就是后端逻辑问题。建议你也这样分层调试效率会高很多。6.2 下一步想做的扩展方向第一版跑通之后我心里其实已经列了一串后续清单。短期会做趋势图表把采集到的历史数据画成折线让 CPU 和内存的变化曲线可视化中期想加一款接入外部系统的卡片比如 Git 分支状态、GitHub Actions 运行记录长期来看我计划把这套数据模型扩展成多端共享移动端可以看其他设备也可以问但前提是把本地版打磨稳定。系列的规划大概是这样第一篇是总体设计与架构后续会拆几篇单独讲探活模块的可靠上报、趋势可视化、以及配置化扩展这些具体功能。这样拆的好处是每个主题都能讲得足够深你也可以按需跳着看。6.3 自造工具的过程中我的最大感受把 Status Deck 从想法变成桌面上天天打开的程序这个过程跟我平时写业务代码的感觉完全不同。业务代码的边界通常是别人定义好的需求、接口、验收标准都在文档里但自造工具时边界需要自己画选型、取舍、模块划分、生命周期管理每一个决策都完全由自己负责。这对我来说是特别好的全栈能力训练因为它强迫我把前端体验、后端稳定、桌面打包、数据治理连成一条线去思考。如果你也想从零造一个自己的桌面工具我的建议是先压小范围想清楚最核心的三个信息展示是什么用最简单的方式跑通再来回迭代。别一上来就想做一套大而全的平台那只会让第一版烂在半路。现在这套代码和数据模型我还在持续调整等后续模块稳定后我会把配置文件模板和 API 约定整理好放出来给想复刻的人一个更顺畅的起点。
返回列表