
上一篇文章写完概念篇之后有不少朋友催更问我到底选了哪些技术、项目最后跑起来没有。这篇就把技术栈选型和实施过程完整复盘一遍。先说结论Status Deck 最终没有用任何“前沿到需要天天看文档”的框架整条链路都是成熟方案的组合核心目的只有一个——让这个面板能长期稳定运行而不是变成一个需要持续维护的“玩具项目”。1. 需求复盘先行技术选型前先想清楚这面板到底要干嘛1.1 我重新定义了“状态”的含义最初构思 Status Deck 时我的想法很膨胀想记录服务器负载、GitHub 提交趋势、每日专注时长、订阅服务到期提醒甚至想塞进一个基于大模型的“状态总结助手”。但真到了选型阶段我把这些需求重新过了一遍最后收敛成三类核心场景。第一类是“系统运维状态”包括本机和几台远程服务器的 CPU、内存、磁盘占用以及各服务的健康检查结果。第二类是“开发进度状态”也就是我在不同仓库里的提交记录、Issue 数量、近七天活跃度。第三类是“生活服务状态”比如域名到期时间、SSL 证书剩余天数、常用 API 的配额余量。这三类场景有一个共同特点数据结构简单、刷新频率不高、展示层面很轻。想清楚这一点后技术选型的范围就完全明朗了——这是个典型的“轻量级全栈应用”不是大数据平台也不是高并发中间件。1.2 我给选型定下的四条硬约束开始挑选技术栈之前我给自己列了四条约束后面一系列决策基本都围绕这四条展开。零外部依赖除服务器本身外尽量不依赖任何第三方 SaaS 服务数据库、缓存、消息推送都要用自托管方案。单机可运行整个项目跑在一台小内存 VPS 或 NAS 上也能流畅运行内存占用控制在 512MB 以内。维护成本优先于性能对于这种单用户、低频次的应用开发效率、部署复杂度、后续维护成本远比性能指标重要。前后端能共享类型定义项目中存在大量“采集数据 → 展示数据”的流转场景如果前后端数据结构各写一份后期改字段会非常痛苦。这四条约束直接帮我排除掉了一批华而不实的选项。比如消息队列用它来传递每分钟一次的采集任务完全属于杀鸡用牛刀再比如 SSR 框架因为 Status Deck 不需要 SEO也不存在首屏加载慢到无法接受的问题纯客户端渲染就够了。2. 技术栈逐项敲定前端、后端、数据层和实时链路的选择过程2.1 前端React TypeScript 的保守选择前端框架这块我其实纠结过一段时间。Vue 3 的响应式模型写起来很顺手Svelte 的代码量更少Solid 的性能很好看但最后我还是选了 React 18 TypeScript Vite 的组合理由一点不浪漫生态最稳、资料最多、出问题最容易搜到解决方案。具体到这个项目我用了 React 但刻意绕开了重状态管理方案。面板应用的状态流非常清晰后端推数据前端渲染用户偶尔调整布局配置。这种场景用 Zustand 做全局状态存储就够了它的 API 极简不需要配置 Provider也没有样板代码。至于数据请求我直接用原生 fetch 封装了一个带超时控制的客户端没有引入 react-query。一个值得展开的点是为什么不用 Next.jsStatus Deck 是一个典型的私有工具面板所有页面都需要登录后访问天生不需要搜索引擎索引。Next.js 的 SSR、ISR 这些能力在这个项目里毫无用武之地反而会把部署复杂度从“静态文件 Nginx”提升到“Node 服务 服务端渲染适配”。纯 Vite SPA 的部署方式简单到令人发指构建完就是一堆静态文件丢到任何 Web 服务器里就能跑。Vite 作为构建工具我没有太多犹豫。Webpack 配置繁琐esbuild 虽然快但生态还不完整Vite 恰好站在了“开发体验好”和“生产构建稳定”的交汇点上。实际用下来冷启动基本在 300ms 以内热更新几乎感觉不到延迟这种反馈速度对开发体验的提升非常明显。2.2 后端Fastify 而非 Express 的理由后端的选择经历了三轮过滤。第一轮是语言层面的取舍Python、Node.js、Go 三者中我排除了 Go因为项目里没有真正的并发热点Go 的类型系统和部署优势在这里体现不出来Python 排除的原因则是类型协作成本偏高前后端共享类型这件事在 Python 生态里做起来比较别扭。第二轮在 Node.js 的框架层面对比了 Express、NestJS、Fastify。Express 的问题在于它太“裸”了中间件生态虽然丰富但所有功能都要自己拼装请求校验、日志格式化、错误处理全得手动搭。NestJS 恰好相反它把架构定得太死依赖注入、模块系统、装饰器这一套对小型项目来说过于隆重学习成本和心智负担都不低。Fastify 在两个极端之间找到了平衡点。它有内置的 JSON Schema 校验机制路由定义时可以直接声明参数结构非法请求会在入口处被拦截不用在业务代码里写一堆 if 判断。它的插件体系很干净日志用的是 Pino性能在 Node.js 框架里属于第一梯队。虽然这些优势在低并发场景下感知不强但开发期的体验差别是实实在在的。实际编码时我还做了一个后来证明非常正确的决定所有对外接口都返回统一的响应结构形如{ code, data, message }。这个结构看起来很老套但在前端调试和错误展示时帮了大忙尤其是 WebSocket 推送和 REST 接口并存的情况下统一的数据信封降低了大量心智负担。2.3 数据层SQLite 的精准定位数据存储是我反复权衡最久的一个环节。一开始我本能地想上 PostgreSQL毕竟它是“标准答案”但深入分析 Status Deck 的数据特征后我得出了一个反直觉的结论这个项目压根不需要独立数据库服务。数据分析一下就很清楚采集任务每分钟最多产生十几条记录一天下来数据量在几千条量级查询模式是“按时间范围取指标序列”数据保留策略是滚动删除 90 天前的记录。这个量级和访问模式SQLite 完全能扛住而且它的优势极其突出。SQLite 是文件型数据库备份就等于拷贝文件不需要单独维护数据库服务不会出现“应用活着但数据库挂了”的连锁故障内存占用几乎可以忽略。更关键的是SQLite 的 WAL 模式在读写并发场景下表现相当够用配合串行化写入队列后完全没有锁冲突压力。当然选择 SQLite 有一个前提整个应用必须单进程运行。我为此在后端架构上做了约束所有采集任务和 API 服务跑在同一个 Node.js 进程里避免了多进程同时写同一个 SQLite 文件的场景。这个约束对单用户面板来说完全合理。关于数据模型我设计了三张核心表指标表存储具体的状态数据点字段包含指标名、时间戳和值面板配置表存用户的布局设置用 JSON 格式存储整个面板的卡片排列数据源表记录各个采集源的健康状态和最近采集时间。三张表的关系非常简单没有复杂的关联查询SQLite 处理这些操作游刃有余。2.4 实时通道WebSocket 的必要与选择Status Deck 的数据刷新有两种路径。一是页面加载时通过 REST 接口拉取历史数据和初始状态二是运行过程中的实时更新通过 WebSocket 推送。我在 SSE 和 WebSocket 之间犹豫了一段时间。SSE 的优点是实现简单基于 HTTP自动重连但它是单向通道只能服务器往客户端推。我在设计面板时有一个需求用户在前端手动刷新某个数据源的采集动作或者临时停用某张卡片这些操作需要从客户端往服务器发指令。虽然这些用 REST 也能实现但既然已经有长连接不如走统一通道。WebSocket 带来的额外收益是双向通信和更低的帧开销。我用ws库实现了一个轻量级的消息中心后端采集器产生新数据后通过内存中的事件总线广播WebSocket 服务监听事件并把消息推送给所有已连接的客户端。前端收到消息后按数据类型更新对应的卡片状态。选型时我还特意避开了 Socket.IO。它的自动降级和房间机制确实方便但多了一层抽象出问题的时候反而难排查。原生 WebSocket 加上自己实现的重连和心跳逻辑代码量也就百来行但整个过程完全可控。3. 项目落地目录结构、接口设计和模块实施顺序3.1 Monorepo 结构与共享类型包项目采用 pnpm workspace 管理的 monorepo 结构。选择 monorepo 的核心原因只有一个共享类型。status-deck/ ├── apps/ │ ├── api/ # Fastify 后端 │ └── web/ # Vite 前端 ├── packages/ │ └── shared-types/ # 共享类型定义与工具函数 ├── docker/ │ └── docker-compose.yml └── package.json共享类型包里的内容很直接定义所有数据传输对象的 TypeScript 类型包括MetricPoint、DataSourceConfig、PanelCardConfig、WsMessage等。前端和后端都通过 workspace 协议引入这个包这样接口字段变更时编译期就能发现所有需要同步修改的地方。这里有个实施中踩过的坑后面会专门展开共享包在源码状态下运行没问题但生产构建时必须产出编译后的 JS 文件和.d.ts类型声明否则各个应用会找不到模块路径。3.2 后端实施顺序先采集后推送再接口后端开发我按“数据从哪来 → 数据怎么存 → 数据怎么出去”的顺序推进这个顺序很关键因为它保证了每一步开发都有可验证的产出。第一步是搭建采集器抽象层。我定义了一个Collector接口任何数据源的采集逻辑都实现这个接口interface Collector { name: string; interval: number; collect(): PromiseMetricSample[]; }接口明确后具体采集器就是纯粹的填充工作。系统指标采集用systeminformation库实现服务健康检查用简单的 HTTP 请求探测GitHub 活跃度通过 REST API 拉取。每个采集器都注册到一个调度器里调度器按各自的interval配置定时执行。第二步是数据落地。采集器返回的MetricSample数组经过一个聚合层处理转换成数据库记录同时更新对应数据源的最近采集状态。这个环节有一个设计细节如果某个采集器连续失败三次聚合层会将该数据源标记为“异常”并把最后一次成功值标记为“过期”。这样前端卡片上能明确区分“当前数据”和“历史残留”避免误导。第三步才是 WebSocket 推送和 REST 接口。推送层监听数据库写入事件把新的数据点广播出去REST 接口提供历史查询、面板配置读写、数据源管理等操作。这个顺序保证了推送和接口开发时数据层已经稳定不会两头一起调试导致问题无法定位。3.3 前端实施顺序渲染骨架优先于真实数据前端开发的顺序恰好和后端相反先把界面和交互全部用 Mock 数据跑通再接真实数据。这样做的好处是能尽早确定数据展示需要哪些字段反过来推动共享类型的定义。前端组件拆分成三层。最底层是基础组件库包括卡片容器、状态指示器、图表组件。图表组件我没有引入任何第三方图表库而是用 SVG 自绘了折线图和环形图。原因很直接面板上的图表形态非常固定就是时间序列曲线和比例环引入 ECharts 或 Chart.js 会把包体积从几十 KB 拉到几百 KB而实际使用的功能不到图表库的百分之五。用 SVG 手绘一个折线图包括坐标轴、网格线和提示框代码量在两百行以内后续自定义样式反而更自由。中间层是卡片逻辑层负责把不同类型的数据源映射到对应的展示组件。这个映射用一张配置表完成卡片类型决定了它订阅哪些指标、用哪种组件渲染、多久更新一次。顶层是面板布局层负责卡片的拖拽排序、增删操作以及布局配置的持久化。布局数据以 JSON 格式存到后端前端加载后按布局渲染。这个方案比用现成的网格库更轻量我只需要一个简单的响应式网格布局不需要复杂的嵌套布局能力。3.4 前后端接口的约定设计Status Deck 的接口设计遵循一个原则REST 管状态WebSocket 流转事件。REST 接口负责初始加载和配置修改包括获取面板全量数据、查询历史指标、保存布局配置WebSocket 只负责实时数据推送和客户端指令转发。协议层面WebSocket 消息统一采用{ type, payload }格式。服务端主动推送的类型有metric:update、source:status-changed、panel:config-updated客户端发往服务端的主要是source:refresh和panel:layout-save。这个协议在设计之初就把消息类型定义在共享包里前后端无需沟通就能保持同步。值得一提的是幂等性问题。WebSocket 属于不可靠传输网络抖动会导致消息丢失或重复。前端对source:refresh这类指令做了去重处理短时间内相同指令只执行一次后端对metric:update广播则做了序列号标记客户端可以根据序列号感知消息连续性。4. 实施阶段踩过的坑四个值得展开的排障过程4.1 WebSocket 频繁断连问题出在 Nginx 闲置超时面板上线后前端日志里出现规律性的 WebSocket 断连大约每 60 秒一次。断连后虽然有自动重连逻辑但每次断连都会导致一次短暂的白屏闪烁体验很差。第一轮排查集中在心跳机制上。我原本的计划是每 30 秒发送一次心跳帧但日志显示断连发生在心跳之前所以心跳不是问题。第二轮排查转向反向代理层果然发现问题我用的 Nginx 配置里没有针对 WebSocket 做超时调整默认的proxy_read_timeout正好是 60 秒长时间没有消息传输时Nginx 会主动断开连接。解决方案分两层。Nginx 增加proxy_read_timeout 3600s和proxy_send_timeout 3600s把代理超时拉长到一小时应用层则保持每 30 秒一次的心跳双保险。这个问题在本地开发时完全不会暴露因为本地不经过 Nginx这也提醒我生产环境的任何中间代理层都可能影响长连接行为调试时一定要把环境差异考虑进去。4.2 SQLite 的database is locked问题使用 SQLite 后我自认为已经规避了多进程写入风险但高峰期仍然出现了SQLITE_BUSY错误。排查后发现问题出在同一进程内的并发写入定时采集器的写入任务和用户手动触发数据源刷新写入同时进行两个写入操作在同一个数据库连接池上产生竞争。解决办法是引入写入队列。所有数据库写入操作通过一个 Promise 链串行执行同一时刻只允许一个写操作。这个方案没有使用额外依赖直接在代码里维护了一个异步队列。从根本上消除了写写冲突。WAL 模式本身解决了读写并发的大部分问题但写写并发仍然是 SQLite 的硬限制。这一点在选型时我就有预期实际实施等同于给当初的选择还了一笔技术债。好在代价很小十几行代码就解决了。4.3 时间显示的历史遗留问题UTC 与本地时区的混乱面板某天突然发现所有图表的时间轴都偏移了 8 个小时。排查后确认采集服务运行在 UTC 时区的服务器上而浏览器在本地时区渲染。一开始我图省事在采集端直接把时间转成了本地时间字符串结果数据到了前端又做了一次本地时区转换导致双重偏移。修复方案是彻底统一时间表示标准后端一律返回 Unix 时间戳不返回格式化字符串数据库存储时间戳整数前端在渲染时统一做时区转换。这个教训在分布式系统领域是老生常谈但在个人项目上特别容易松懈因为单机环境太容易让人忽略时区问题。4.4 Monorepo 共享包在生产构建时的路径解析故障本地开发一切正常执行生产构建时apps/api报了一堆Cannot find module status-deck/shared-types的错误。原因是共享包在开发模式下通过 pnpm workspace 软链接到源码目录Node.js 可以直接解析 TypeScript但生产构建时 TypeScript 编译器需要解析实际的文件路径而共享包没有dist目录构建必然失败。解决方案是在共享包里配置了完整的构建流程先执行tsc编译产出dist目录再在package.json里声明main和types指向编译产物。这个坑虽然不复杂但排查过程提醒我monorepo 的本地开发体验再顺畅也不能跳过生产构建的验证环节环境差异永远是前端项目最大的不确定性来源。5. 部署结构与日常运转让面板真正跑起来5.1 精简的双容器部署最终的部署架构是 Docker Compose 编排两个容器一个运行后端服务一个运行 Nginx。前端静态资源在构建阶段被打包进 Nginx 容器的镜像里所以实际运行时的服务发现关系是Nginx 托管静态页面并把/api和/ws路径反向代理到后端容器。services: api: build: ./apps/api restart: unless-stopped volumes: - ./data:/app/data web: build: ./apps/web restart: unless-stopped ports: - 8080:80 depends_on: - api数据目录通过 Volume 挂载出来备份这个面板的数据只需要拷贝一个文件夹配合 cron 定时任务打包上传就行整个备份体系不需要任何额外工具。5.2 数据保留与滚动清理SQLite 数据库的体量增长很慢大约每天增加几百条记录三个月的数据累计也就几 MB。即便如此我还是写了滚动清理逻辑每天凌晨自动删除 90 天前的历史数据保证数据库体积稳定。清理逻辑直接放在调度器里作为一个特殊的“系统采集器”运行保持整个代码结构的一致性。5.3 面板的运行频率与资源消耗实际运行下来全栈服务的常驻内存占用约 180MB其中后端 Node.js 进程占大头前端静态资源完全由 Nginx 提供不额外消耗内存。采集频率方面系统指标每 30 秒一次服务健康检查每 1 分钟一次GitHub 数据每 5 分钟一次整体对服务器的 IO 压力可以忽略。这个资源占用水平意味着它可以和博客、其他小型服务共存于同一台服务器上不需要单独开一台机器。Status Deck 作为个人工具的核心价值在于“低存在感”部署完就安安静静地运行不需要日常关心它有没有挂掉。6. 实施后的体会与下一步想加的东西6.1 回头看选型哪些决策被证明正确哪些有待商榷整个实施周期结束后我对当初的技术选型做一个诚实的复盘。正确且坚决的决定有三个。SQLite 作为数据层是最明智的选择项目运行半年多从未出现数据损坏或性能问题前端不去引入重型图表库节省了包体积和开发时间SVG 自绘的方案完全覆盖了需求共享类型包的 monorepo 结构让前后端协作变得极其顺畅尤其是之后想加新卡片类型时改完类型编译一次两边都能立刻发现问题。有待商榷的决定也有两个。一个是 WebSocket 协议自己实现的心跳与重连机制虽然代码可控但 Socket.IO 等成熟库确实提供了更完善的重连策略如果未来要支持多客户端自研方案还需要加固。另一个是 Fastify 的 Schema 校验对于接口很少的小项目来说这个特性带来的收益有限但校验代码本身也有维护成本。6.2 下一步迭代方向从“能跑”走向“好用”当前的 Status Deck 已经满足了我最初设想的所有场景但使用一段时间后我列出了几个想在第三篇文章中实现的方向。首先是数据源插件化让新增一种数据源的流程变成“写一个 Collector 接口的实现类注册到配置中心”而不是改核心代码。其次是面板的自定义表达式支持类似“磁盘占用率大于 80% 时高亮显示”的简单规则这能把面板从纯展示工具升级成带告警能力的监控站。最后是访问控制的完善目前面板通过简单的认证令牌保护后续想加上基于角色的访问控制为未来开放给少数朋友使用做准备。6.3 给类似自造项目的三点经验总结最后分享三点对整个项目影响最大的经验。第一个人项目的技术选型应该以“自己不烦心”为第一原则。技术栈的新颖程度不会让数据采集变得更准确但维护成本和部署复杂性会直接影响你长期使用的意愿。很多项目死在中后期的维护倦怠而不是死在初始功能不强大。第二数据结构定义值得在编码前多花时间。Status Deck 之所以迭代得顺手很大程度得益于前后端共享类型的设计在早期就确定了。对个人项目来说定义一套清晰的数据流转协议比用什么框架重要得多。第三生产环境的问题要放到生产环境的条件下去验证。WebSocket 断连和 SQLite 锁冲突这类问题开发环境永远复现不出来因为少了反向代理和真实并发这两个变量。自造项目虽然规模小但排障思维不能省。Status Deck 这个项目做到现在最大的收获其实不是面板本身而是完整经历了一遍“从需求收敛到技术选型再到实施落地的全链路决策过程”。下一篇文章我会重点写自定义告警规则和数据源插件化的具体实现到时候再继续聊。