缘起
第一个桌面客户端发出去,收到的反馈是「打不开」,而我这边跑得好好的。想定位就得知道它崩在哪一步,可应用连一行日志都没往盘上落。那之后我给自己定了条规矩:任何桌面项目,先让它在别人的机器上说得清自己死在哪。
背景
桌面客户端项目每次起盘,操作系统这一层都要重新打交道:窗口生命周期、单实例锁、系统托盘、日志轮转、配置读写、崩溃捕获、 打包安装。这些与业务无关,但每一件都要做一次决定,而每次决定都可能和上一个项目不一致。需要一套与业务 无关、可反复复用的桌面基座,把这些"管道"沉淀下来,作为后续所有桌面项目的起点。
方案
基座的定位是「只给管道,产品面下游自己长」——它提供运行时能力与契约,不预置业务页面:
- 运行时能力:单实例锁(二次启动唤起已有窗口)、系统托盘 + 右键菜单(Windows/Linux)、
日志轮转(5MB × 3 份)、
config.json读写、原生对话框、panic 落盘、优雅关闭 - 管道契约(Go 与 TS 两端单一真相在 Go):
- 错误协议
internal/apperr+ WailsErrorFormatter→ 前端拿到{code, message} - 分页协议
internal/page的Request/Result[T] - 事件协议
internal/event的<domain>:<action>命名约定
- 错误协议
- 架构护栏:把约定编译成"会红的检查"——分层越界、模型注册双向校验、依赖登记制
(
deps.yaml双向校验)、前端 import 安全与硬编码色值,随make test/make lint执行; 护栏"感知自己瞎了":解析到 0 个结果会 Fatal 而非静默放行 - 模块生成器:
make gen name=<模块>从黄金范例_example/生成 model + service + 绑定 + 页面 + 路由 + 菜单,锚点注入且幂等 - 打包发布:
make package产出 macOS 美化 dmg(Applications 软链 + 背景图合成 + 箭头 同源对齐 + 回读校验)或 Windows NSIS 安装器(scope=user|machine)
技术架构
| 模块 | 技术选型 |
|---|---|
| 后端 | Go ≥ 1.25 + gorm + SQLite(纯 Go 驱动,交叉编译无 CGO) |
| 桌面运行时 | Wails v2(WebView2 / WKWebView) |
| 前端 | Vue3 + Vite + Element Plus + Vue Router(hash 模式适配 file://)+ Pinia |
| 设计系统 | 暗色优先,单主色派生全色阶(tokens.css) |
| 工程化 | Makefile 统一入口 + 架构护栏(AST / ESLint)+ 依赖登记制 + OpenSpec 治理 |
采用五层分层(L1 桌面运行时 → L2 前端渲染 → L3 绑定方法 app.go → L4 业务服务 →
L5 数据存储),app.go 不直接 import gorm/database,业务一律经 service,由护栏强制约束。
成果
形成了可反复复用的桌面客户端母版:scripts/init.sh myapp 一键实例化(替换模块名占位符
__APP_NAME__ 并清空归档历史),make gen 生成业务模块骨架,make test/lint/smoke 靠护栏
与冒烟证明"没改坏"。macOS 产出带拖拽引导的美化 dmg,Windows 产出免管理员的 NSIS 安装器,
目标机器无需 Go / Node.js 运行时。
已知限制
- macOS 无系统托盘:Wails v2 的 NSApplication delegate 与所有 systray 库冲突,故 macOS 关闭即退出,托盘能力仅在 Windows/Linux 提供
- 无 headless 模式:冒烟测试定位为本地验证,CI 只编译不启动 GUI
- 母版防呆:含
__APP_NAME__占位符时拒绝打包,需先执行scripts/init.sh实例化
