
作为一个常年跟终端、IDE、浏览器标签页“多线作战”的开发者我早就受够了每天开工前的那套固定动作打开终端看一眼本地服务起没起切到Git平台查一下CI跑没跑再开个监控页面确认服务器负载……等把这些状态都确认完半小时已经进去了。所以当我在自己的项目堆里翻到“Status Deck”这个记录时第一反应就是——这东西终于可以把我从这一堆状态确认里解放出来了。这个Status Deck说白了就是一个给开发者自己用的桌面仪表盘。它不是那种大而全的团队监控系统而是把你自己电脑上的资源占用、本地开发服务的存活状态、常用HTTP接口的响应情况甚至某个目录下的Git分支状态统一收进一个桌面窗口里一眼就能看全。整个项目从系统信息采集、服务探测到后端接口、前端展示再到桌面端封装链路全部自己搭没有借第三方SaaS所以叫“全栈自造”。这系列文章我打算按模块拆开来写第一篇先把骨架和核心玩法讲清楚适合那些想从零练手全栈、又不想再做一遍“待办事项”式Demo的人参考。1. 项目初衷与整体思路拆解1.1 为什么不是现成工具而是自造一个市面上能显示系统状态的小工具其实不少任务管理器、资源监视器、各种仪表盘软件一抓一大把。真让我下决心自己写的是这几个场景它们都覆盖不全一是“本地开发服务”的存活状态。我常用的是localhost:5173、localhost:3000、localhost:8080这类地址后台API跑在localhost:8000数据库可能在localhost:5432。这些服务不会都在同一个工具里被统一探测任务管理器只能看到进程在跑却不会告诉你“这个进程在监听哪个端口、对应的HTTP接口是不是真的能通”。二是“跨数据源的聚合展示”。系统CPU、内存占用是一类数据服务响应状态是一类数据Git分支状态又是一类数据。现成的桌面插件一般只做单一领域想全部塞进同一个界面几乎找不到合适的产品。三是“自定义探测逻辑”的灵活性。比如我想定时请求某个接口看它的响应时间是不是超过500毫秒或者想探测某个TCP端口是否开放甚至想在自己电脑上跑一个定时任务把结果丢到仪表盘上展示。这类需求很个人化现成工具根本不会给你留这种扩展口子。自造这个Status Deck本质上不是为了“炫技”而是为了把“我每天到底要看哪些状态”这件事彻底固化成一个工具。数据源、展示方式、刷新频率全部由自己控制这是现成方案给不了的。而且整个链路不长前端、后端、采集、桌面封装都走一遍练完这一套对全栈项目的结构感和边界感会清楚很多。1.2 核心场景与需求边界动手之前我给Status Deck划了三条需求边界后面所有设计都围绕这三条展开第一面向单机自用不做多用户、不做权限管理。它就是跑在我自己电脑上的一个桌面应用不需要后台账户体系。这个边界很重要它帮我砍掉了大量和核心功能无关的复杂度。第二数据源以“本机系统信息 本地服务探测”为主。CPU、内存、磁盘这类数据通过系统API直接读取本地开发服务的存活状态通过端口探测或HTTP请求来检测。范围再大一点是远程服务器的状态但第一版先不做避免把项目撑爆。第三展示侧要“一眼可见”。所有卡片必须能在三秒内读完状态颜色要足够醒目数据刷新不能有明显卡顿。仪表盘类工具最忌讳的就是信息堆成一片大海用户盯半天找不到重点。总结成一句话就是做一个启动快、信息够、不给自己添麻烦的桌面状态聚合器。明确边界之后再去想技术方案思路就顺很多。2. 技术方案与全栈架构设计2.1 技术选型为什么是这套组合Status Deck的技术栈我最终定为桌面壳用Electron前端用Vue 3 Vite后端用Node.js Express数据采集用Node原生模块加少量child_process命令。这套组合的核心考量是“只用一种主力语言——JavaScript”把全链路的认知负担压到最低。说句实在话这个项目如果拆开看每一个环节都有更“高级”的方案。比如桌面端可以上Tauri用Rust写核心逻辑打包体积小、内存占用低后端可以换成Go或Python FastAPI性能更好前端也可以用React或Svelte。但问题是这个项目的第一目标不是极致性能而是“我一个人能维护、能快速迭代”。JavaScript从头写到尾意味着每一层我都能畅通无阻地改不需要在切换语言上下文上花时间。Electron在这套组合里承担的职责比较纯粹它提供一个Node.js运行时和一个Chromium渲染窗口。我的Vue前端构建后是一个静态目录由Electron的主进程加载后端Express服务独立跑在localhost的一个端口上前端通过HTTP请求拿数据。Electron在这里其实没有做太多“深度整合”更多是扮演一个壳——但这不影响它的价值因为桌面应用该有的托盘、窗口管理、开机启动这些能力它都天然具备。2.2 数据流一次完整的状态刷新链路整个Status Deck的数据流是单向的、清晰的一条线采集层在Node进程内定时执行负责读取CPU使用率、内存占用、磁盘空间、服务端口状态、HTTP接口响应时间等数据。采集结果被整理成一个统一结构的JSON对象。后端Express启动时挂载若干路由其中最关键的是一个聚合接口GET /api/status每次被请求就触发一次全量采集然后返回最新快照。前端Vue应用启动后通过setInterval每三秒请求一次/api/status拿到数据后驱动各卡片组件更新渲染。有人可能问为什么不用WebSocket或者Server-Sent Events推送数据而是用轮询答案很简单这个场景下3秒一次的轮询已经足够实时实现复杂度比WebSocket低了一个量级而且出问题的时候非常好排查——你只需要打开浏览器直接访问/api/status看返回的JSON正不正常就知道问题出在哪一层。WebSocket在“需要即时双向通信、消息频率高”的场景才是首选而状态仪表盘是一个典型的低频读取场景轮询反而更稳。这条链路的设计还有一层重要的考虑——分层可替换。如果哪一天我不想用Electron了直接让Chrome打开Vite的dev地址也能用如果我不想用Vue了后端接口完全不用动写个最简单的HTML页面也能接。每一层之间只靠HTTP和JSON通信互不绑架这在我后续迭代时帮了大忙。2.3 目录结构与模块划分项目仓库的目录结构我按“桌面壳 后端服务 前端界面”三块来组织status-deck/ ├── electron/ # Electron主进程与预加载脚本 │ ├── main.js │ └── preload.js ├── server/ # Express后端 │ ├── index.js # 服务入口路由挂载 │ ├── collectors/ # 数据采集器 │ │ ├── system.js # CPU/内存/磁盘 │ │ ├── services.js# 本地服务探测 │ │ └── health.js # HTTP健康检查 │ └── utils.js # 公共工具函数 ├── web/ # Vue3前端 │ ├── src/views/ # 页面组件 │ ├── src/components/# 卡片组件 │ └── vite.config.js └── package.json这个目录划分的核心原则是“按职责拆不是按功能拆”。Electron目录只负责桌面壳的生命周期管理server目录只负责数据采集和接口提供web目录只负责UI渲染。这样划分之后每一块的定位都非常单纯。我自己踩过的最大的坑恰恰是反过来——把后端逻辑写进Electron主进程里导致排查问题时哪哪都是纠缠在一起的逻辑基本等于没法维护。所以强烈建议按这个思路来组织越早想清楚越好。3. 核心采集模块的实现细节3.1 系统信息采集用Node原生模块拿CPU和内存系统信息采集是Status Deck里最基础的一块。Node.js的os模块提供了丰富的系统API不需要安装任何第三方依赖就能拿到绝大部分关键数据。CPU使用率不能直接通过os.cpus()拿到一个现成的“当前使用率”——这个方法返回的是每个CPU核心的运行时间统计times对象包含user、nice、sys、idle、irq几个字段。要计算使用率标准做法是“两次采样法”第一次采样记录各核心的idle和总时间等待一个短间隔比如100毫秒再采样一次然后用两次的差值算出这个间隔内的CPU使用率。const os require(os); let lastCPU os.cpus().map(c ({ idle: c.times.idle, total: Object.values(c.times).reduce((a, b) a b, 0) })); function getCPUUsage() { const current os.cpus().map(c ({ idle: c.times.idle, total: Object.values(c.times).reduce((a, b) a b, 0) })); const usage current.map((c, i) { const idleDiff c.idle - lastCPU[i].idle; const totalDiff c.total - lastCPU[i].total; return totalDiff 0 ? Math.round((1 - idleDiff / totalDiff) * 1000) / 10 : 0; }); lastCPU current; return usage; }注意这里返回的是一组百分比每个核心一个值展示的时候我会取其平均值作为整体CPU使用率。两次采样的间隔太短会导致波动剧烈太长会导致响应迟钝100毫秒是我调过几次之后觉得比较合适的值。内存和磁盘就简单多了os.totalmem()、os.freemem()直接给字节数磁盘空间用fs.statfs或node:fs的statfsSync也能拿到总空间与剩余空间展示时统一换算成GB并保留一位小数。系统采集这块最大的注意事项是“不要每次请求都现采样CPU”。我一开始没做缓存前端3秒钟请求一次每次都会触发两次CPU采样虽然影响不大但Electron窗口里如果同时开着动画偶尔能看到CPU曲线出现突兀的尖峰。后来我改成每5秒在后台做一次采样缓存接口直接返回缓存结果数据反而更平滑了。3.2 服务状态探测端口、进程与HTTP三级检查服务状态探测是Status Deck的核心玩法。我给每个服务配置了一个检查策略分三个层级第一级是TCP端口探测。判断某个端口比如3000、8000是否处于监听状态是最快的存活信号。Node里实现TCP探测我直接用net模块建立一次连接尝试——能连上说明端口有进程在监听连不上说明服务挂了。const net require(net); function checkPort(port, host 127.0.0.1, timeout 2000) { return new Promise((resolve) { const socket new net.Socket(); socket.setTimeout(timeout); socket.once(connect, () { socket.destroy(); resolve(true); }); socket.once(timeout, () { socket.destroy(); resolve(false); }); socket.once(error, () resolve(false)); socket.connect(port, host); }); }第二级是HTTP健康检查。端口通不代表服务真能用——很多应用监听端口没问题但内部某个依赖已经挂了接口开始返回5xx。这时候可以用http.get或fetch发一个真实请求判断状态码和响应时间。响应时间超过我设置的阈值比如3000毫秒我会标记为“响应慢”在卡片上用黄色提示这比纯“通/不通”更有信息量。第三级是进程匹配用于确认“端口后面的进程是不是我期待的那一个”。我用child_process.exec执行lsof -i :8080或netstat -ano | findstr :8080来拿PID再把PID和已知的进程名做匹配。这个层级不是每次探测都做——它比较重我只在服务状态异常时触发用于快速定位是“端口被别的程序占了”还是“我的服务没起来”。服务探测的设计里最值得讲的一个坑是“端口探测成功但HTTP请求失败”的场景。比如端口被一个无关进程占用TCP探测会返回true但按期望路径请求接口却拿不到正确响应。所以我的判断逻辑是HTTP检查优先只有当服务没有配置HTTP检查路径时才把TCP探测作准。这个先后顺序如果你反过来仪表盘上就会出现“绿色但实际不可用”的虚假繁荣这种状态比什么都不显示更误导人。3.3 Git状态与自定义命令让数据源不设限除了系统和服务的标准指标Status Deck还支持“自定义命令抓取”——从本质上讲就是提供一个通用入口把任何一条Shell命令的标准输出变成仪表盘上的一个数据点。一个典型的用法是Git分支状态。我经常想知道某个项目目录当前在哪个分支、工作区干不干净。这个信息没有现成的Node API但一条git status --porcelain -b就能拿到。我用child_process.exec去执行这条命令把输出的第一行解析成分支名如果有别的行就标记为有未提交更改。const { exec } require(child_process); function getGitStatus(projectPath) { return new Promise((resolve) { exec(git status --porcelain -b, { cwd: projectPath, timeout: 3000 }, (err, stdout) { if (err) { resolve({ branch: 无法读取, dirty: false, error: err.message }); return; } const lines stdout.split(\n).filter(Boolean); const branchMatch lines[0] lines[0].match(/^## (.?)(\[|$)/); resolve({ branch: branchMatch ? branchMatch[1] : 未知分支, dirty: lines.length 1, }); }); }); }这个功能的价值在于它把Status Deck从“监控工具”变成了“个人工作台”。我可以在配置文件里定义任何我想看的指标——磁盘里某个目录的占用空间、某个Python虚拟环境里依赖包的版本、甚至是当前网络的外网IP归属地只要能用命令表达都能上仪表盘。这也呼应了整个项目的定位它是给你自己用的边界由你自己划。4. 后端接口与前端仪表盘实战4.1 Express后端接口设计与定时预聚合后端部分我用Express起一个轻量服务只干两件事对外提供状态接口对内统一下发“采集任务”。服务通过setInterval每10秒在后台做一次全量采集把结果存进内存变量接口请求到来时直接返回缓存快照而不是临时触发采集。这个设计的考量是多个指标一次采集完之后是同一时刻的数据快照前端看到的所有卡片状态是自洽的如果每个接口请求都单独触发采集不同卡片的数据会来自不同时刻排查问题时容易造成误判。后端接口我目前暴露了三个主要路由GET /api/status返回系统信息CPU、内存、磁盘加所有服务状态端口、HTTP响应时间、进程匹配结果GET /api/services只返回服务卡片所需的数据方便前端单独刷新某个区域GET /api/health是Electron主进程用来做“后端是否就绪”的判断防止前端窗口打开时后端还没起来导致白屏。const express require(express); const app express(); let snapshot { system: {}, services: {}, updatedAt: null }; async function refreshSnapshot() { snapshot { system: await collectSystemInfo(), services: await collectServices(), updatedAt: Date.now(), }; } app.get(/api/status, (req, res) res.json(snapshot)); app.get(/api/health, (req, res) res.json({ ok: true })); setInterval(refreshSnapshot, 10000); refreshSnapshot(); app.listen(18765, 127.0.0.1, () { console.log([status-deck] backend listening on http://127.0.0.1:18765); });端口我特意选了18765这种不太常见的号码而不是8080、3000这类开发工具高频占用端口因为本地开发环境里端口冲突的概率比我预想的高得多。监听地址固定为127.0.0.1避免暴露到局域网里——这是一个仪表盘数据只给自己看没必要开成服务让别人访问。4.2 Vue 3前端卡片布局与状态颜色映射前端整体是一个典型的“仪表盘”布局顶部一条系统状态栏下面是服务卡片区。系统状态栏用一排紧凑的指标组件呈现CPU、内存、磁盘占用率每个组件都有一条细进度条服务卡片区用CSS Grid做auto-fill自适应网格每个服务一张卡片卡片上显示名称、地址、当前状态、响应时间、最后检查时间。组件结构上我拆了三个核心组件StatusBar.vue负责顶部系统状态ServiceCard.vue负责单张服务卡片App.vue负责全局数据拉取和消息分发。数据拉取逻辑集中在App.vue里用setInterval每3秒请求一次import { ref, onMounted, onUnmounted } from vue const status ref(null) let timer null async function fetchStatus() { try { const res await fetch(http://127.0.0.1:18765/api/status) status.value await res.json() } catch (e) { status.value { error: 后端连接失败 } } } onMounted(() { fetchStatus() timer setInterval(fetchStatus, 3000) }) onUnmounted(() clearInterval(timer))状态颜色的映射规则是我花心思最多的地方。我统一用三色体系绿色表示正常黄色表示“警告但可用”比如响应时间偏慢、磁盘空间低于20%红色表示“不可用”或“严重异常”端口不通、HTTP 5xx、CPU长期高于90%。前端定义一个纯函数mapStatusToColor(status)把后端传来的状态值映射到颜色和提示文案展示层绝不直接依赖数字阈值——因为阈值可能会变后端改阈值的时候前端不需要重新发版。4.3 Electron封装主进程、预加载与窗口创建最后一层是Electron壳。虽然Status Deck本质上是一个Web应用但Electron给了它几个Web页面没有的能力作为桌面应用常驻、在系统托盘显示图标、开机自启、以及“不用先手动开一个终端跑服务再开浏览器”的完整启动体验。Electron主进程的关键逻辑是启动时先fork出Express后端进程等/api/health能通之后再创建BrowserWindow加载构建好的前端文件。窗口创建完主进程不退出而是常驻后台加上托盘图标和退出菜单。关闭窗口时我可以选择“最小化到托盘”而不是真正退出这样服务探测还在后台跑着需要用的时候点一下托盘图标就弹出来。这里有一个值得注意的实现细节Express服务不要直接写在Electron主进程里而是用child_process.fork()单独跑一个子进程或者直接spawn一个Node脚本。为什么因为Electron主进程一旦崩溃如果服务也在其中那就一起没了而主进程要是因为某个采集器卡住而阻塞也会影响整个应用的响应。拆成独立子进程后两者互不影响采集器再脏也不会拖垮桌面窗口。5. 常见问题与排查技巧实录5.1 服务状态一切正常图表区域数据却一直空白这是我调试过程中遇到最频繁的一个问题——系统信息正常显示但卡片区域的数据始终不出来。排查了半小时最后发现是后端返回的services字段结构里服务数组的key命名和前端组件里访问的字段名不一致。比如后端返回的是response_time前端用的是responseTime下划线命名和驼峰命名没对齐前端拿到的就是undefined。排查这类问题最快的办法是直接在浏览器里访问http://127.0.0.1:18765/api/status把JSON贴到VSCode里对照字段名逐个检查。这个经验看起来很笨但真的能省下大把时间。我的后端在返回快照时专门加了一个schemaVersion字段就是为了防止前后端版本不匹配导致字段错位。5.2 端口探测显示“在线”但对应服务实际不可用这个问题我在3.2节提过——TCP端口通不等于服务真的健康。排查思路是逐层剥离先做一级TCP探测确认端口在监听再做二级HTTP请求确认能拿到预期响应最后看进程匹配确认监听者是我想要的那一个。这三个层级全部通过才算“健康”。我还曾经遇到过一个很有意思的情况RabbitMQ的5672端口是通的但它内部的某个队列节点已经挂了HTTP管理端口响应是500。如果光看5672端口Status Deck会显示绿色非常误导。后来我把服务配置里加了一个healthPath字段要求必须请求/api/health这类健康检查端点才算通过情况才好了很多。5.3 前端轮询导致频繁闪烁另一个体验问题每3秒一次的数据更新如果每次更新都把整个组件重新渲染会导致卡片内容频繁闪烁CPU曲线的动画也明显不流畅。这个问题本质上是“数据更新粒度”和“视图渲染粒度”不匹配。我采取的优化是“引用更新”后端每次采集都生成一个新的快照对象但快照里单个服务的结果如果和上一次完全相同就复用旧的对象引用。前端Vue的响应式系统这时候会发现“cachedService对象本身没有变化”从而跳过该组件的重新渲染只有真正变化了的卡片才会刷新。这样实现下来视觉效果上就只剩“数据平滑变化”卡片不会整体闪动CPU占用也更低。5.4 常用问题速查一次讲清四个高频报错我把这段时间调试中踩过的坑整理成一张速查表后续如果遇到同类问题可以直接按图索骥现象可能原因排查与解决前端页面白屏构建产物路径不对或后端未启动先访问/api/health再检查Electron加载的dist/index.html路径是否解析正确卡片一直显示红色“不通”服务地址配置错误或探测超时时间过短手动用curl确认服务地址可访问检查探测timeout配置本地慢服务建议放宽到3秒以上CPU数据偶尔出现尖峰每次请求都重复执行两次采样采样间隔过短把采集改为后台定时任务接口返回缓存快照采样间隔建议大于500毫秒后端端口被占用本地已有程序占用自定义端口切到另一个空余端口确认监听地址为127.0.0.1避免局域网冲突调这个项目的过程其实很像一个放大镜——它把全栈开发里所有零散的“麻烦事”都集中在一个小项目里发生一遍跨域问题、字段命名不一致、进程生命周期管理、前端渲染性能优化、桌面壳与后端服务的耦合边界……每解决一个记录一次下一次再遇到同类问题就能直接秒杀。从第一行代码到Status Deck能稳定运行我自己最大的体会是做一个工具型全栈项目真正值钱的不是某一个技术点用得有多炫而是你把“从数据源头到用户界面”这条链路想清楚、理顺了。链路清晰了后面的所有问题都是局部问题逐个击破就好。这个系列后续我打算把“远程服务器监控”“WebSocket实时推送”“更多自定义插件的接入模板”这几个部分逐一展开如果你们在实践中也遇到了什么有意思的问题很欢迎按照文中的框架自己复现一遍踩上几个不一样的坑——那才是最赚的部分。