第 2 章:工程骨架与依赖方向
目录很多并不可怕,危险的是把目录名当成架构。本章从依赖方向出发,将项目划分为产品界面、IPC 门面、领域编排、基础设施与独立 crate,解释每一层可以知道什么、不应该知道什么。
一、顶层不是前后端二分
仓库顶层看起来像典型 Tauri 项目:
clash-verge-rev-dev/
├── src/ React + TypeScript
├── src-tauri/ Tauri 应用 crate
├── crates/ 可复用 Rust crates
├── scripts/ 开发、打包、更新脚本
├── template/ 发布模板
├── tests/ 前端集成测试
└── .github/workflows/ 多平台 CI/CD但后端内部不是一个平铺的“API 层”,而是至少五种职责:
cmd → feat → config / enhance / core → utils / plugins / OS前端也不是简单的 pages → components → api:
pages → components → hooks → services → Tauri/Mihomo
↘ providers / external stores / workers二、Rust 主应用的职责层
2.1 cmd/:IPC 适配层
cmd 中的函数由 tauri::generate_handler! 注册。它应当负责:
- 接收可序列化参数;
- 调用领域函数;
- 将错误转换为前端可接收的字符串;
- 不持有复杂业务状态。
例如 Profile command 不应该自己实现下载重试,而是把工作交给 feat::profile 或 config::profiles。
2.2 feat/:用例编排层
feat 对应用户可感知的动作:打补丁、切 Profile、更新订阅、切节点、退出应用、保存监听端口。它连接多个内部模块,决定副作用顺序。
feat/config.rs 是典型例子。一个 IVerge patch 会先被转成 UpdateFlags,再选择性执行:
- restart core;
- reload config;
- 更新自启动;
- 重新应用系统代理;
- 更新热键、托盘、语言、日志和轻量模式。
这里的关键不是 bitflags 本身,而是把“字段变化”翻译成“必须执行的副作用集合”。
2.3 config/:持久状态与候选状态
Config 聚合四类配置:
| 字段 | 类型 | 角色 |
|---|---|---|
clash_config | Draft<IClashTemp> | 应用维护的 Mihomo 合并配置 |
verge_config | Draft<IVerge> | 桌面应用自身设置 |
profiles_config | Draft<IProfiles> | Profile 索引、当前项和增强项 |
runtime_config | Draft<IRuntime> | 增强流水线的产物 |
它既是状态仓库,也是配置生成入口,但没有直接处理页面。
2.4 enhance/:纯变换优先的编译管线
enhance() 收集 Profile 与应用设置,然后按严格顺序变换 serde_yaml_ng::Mapping。内部函数尽量接收参数并返回新 Mapping,例如:
use_seqmerge_default_configuse_mergeuse_scriptuse_tuncleanup_proxy_groupsuse_sort
这让复杂的优先级可以通过单元测试固定,而不是隐藏在全局状态和 I/O 中。
2.5 core/:外部世界的协调器
core 负责所有“不是改一个内存对象就能完成”的工作:
- 启停 Mihomo;
- 服务安装、探测和所有权监控;
- 运行状态机;
- 系统代理和 TUN 能力;
- 配置验证与热加载;
- 托盘、热键、定时器、日志、更新器;
- 向前端发事件。
它是后端最大区域,也是并发与故障恢复最密集的区域。
2.6 utils/:平台和基础设施
这里包含目录解析、嵌入 HTTP 服务器、单例检测、网络探测、窗口管理、DNS 恢复、Windows 计划任务、Linux MIME 等。一个实用判断是:
如果代码回答“在某个平台上怎样做”,它多半属于
utils;如果回答“产品在何时做”,它属于feat或core。
三、独立 crates:把稳定机制从产品壳中抽出
workspace 包含六个本地 crate:
| crate | 核心职责 | 为什么独立 |
|---|---|---|
clash-verge-draft | COW 快照、草稿、事务 | 配置一致性的通用机制 |
clash-verge-logging | 日志类型、宏、过滤和 sidecar writer | 避免主 crate 到处重复格式 |
clash-verge-signal | Unix/Windows 退出信号和 latch | 平台差异清晰、可测试 |
clash-verge-i18n | Rust 侧语言解析和翻译 | 托盘/通知不依赖前端 i18n |
clash-verge-limiter | 原子限流器 | 与业务无关,可注入时钟测试 |
tauri-plugin-clash-verge-sysinfo | 系统/应用信息和诊断导出 | 以插件状态注入 Tauri |
这种拆分并不追求“每个模块都发包”,而是把稳定、可测试、依赖少的机制与产品编排隔开。
四、前端的六层
4.1 pages/:路由边界
八个页面分别是 Home、Proxies、Profiles、Connections、Rules、Logs、Unlock、Settings。_navigation.tsx 同时定义菜单和路由组件,_routers.tsx 将它们映射为 React Router 配置。
4.2 components/:产品组件
组件按领域拆分,而不是按 Button/Card 这类视觉原子拆分:
profile/处理 Profile 卡片、编辑器和结构化编辑;proxy/处理组、链、节点、provider 和虚拟渲染;connection/处理列模型、表格与详情;setting/mods/每个文件对应一个配置子域;home/包含仪表盘卡片与 Canvas 流量图。
base/ 才是可复用 UI 基础件。
4.3 hooks/:状态和副作用复用
Hooks 不只是包装 SWR。它们还承担:
- WebSocket 生命周期;
- 可见性控制;
- 延迟测试去重;
- 代理选择的 latest-wins 队列;
- 连接快照结构共享;
- 主题、窗口与系统状态派生。
4.4 services/:外部边界
cmds.ts 是 Tauri invoke 门面;api.ts 访问公网服务;events.ts 统一前端可见事件;query-client.ts 把 SWR 封装成项目的 query API;preload.ts 在 React mount 前加载最关键配置。
4.5 providers/:少量全局上下文
项目没有把所有服务器状态塞进 Context。Context 只承载应用级快照和窗口能力,服务器数据主要留在 SWR 或外部 store 中,避免高频更新让整棵树重渲染。
4.6 utils/ 与 Worker
URI parser、流量采样器、搜索匹配和系统判断留在 utils。流量压缩进一步放入 Web Worker,主线程保留 fallback 实现。
五、真正的边界:谁可以直接操作谁
5.1 前端不能直接碰文件和进程
即使 Tauri 插件提供 fs/shell,产品操作仍主要经过 Rust command。这样可以集中做:
- 路径约束;
- 配置校验;
- 权限判断;
- 事务与回滚;
- 日志和通知。
5.2 enhance 不应该拥有运行时副作用
增强管线的目标是产生 Mapping。少量历史原因下,TUN 在 macOS 会触发 DNS 恢复,但总体设计仍努力把变换写成纯函数。越纯,优先级和幂等性越容易测试。
5.3 RunState 是运行事实的唯一聚合点
CoreManager 监督子进程,ServiceManager 管特权服务,但对外展示的 mode + health + pending + capability 由 RunState 统一派生。否则托盘、设置页和启动逻辑会各自解释服务状态,最终互相矛盾。
六、平台差异如何控制扩散
项目大量使用编译期 cfg:
- Windows:Job Object、Windows Service、UWP、WebView2;
- macOS:LaunchDaemon/LaunchAgent、activation policy、系统代理交给 Service;
- Linux:systemd、redir/tproxy、MIME 和 Wayland/WebKit workaround。
好的做法是把差异收敛在同名函数后面,例如各平台都提供 install_service() / uninstall_service(),上层只面向统一动作。并非所有文件都做到了这一点,但整体方向清晰。
七、判断架构是否健康的三个问题
- 一个新页面是否必须知道 Service IPC token?不应该,它只应看到
RunStateView。 - 一个配置合并函数是否必须读取 Tauri AppHandle?通常不应该,它应接收参数并返回 Mapping。
- 一个系统代理实现是否可以自行决定何时运行?不应该,时机属于 CoreManager/feat,平台代码只实现动作。
这三个问题分别守住产品、领域和基础设施边界。
本章源码索引
src-tauri/src/cmd/mod.rs:Tauri command 导出src-tauri/src/feat/mod.rs:产品用例导出src-tauri/src/feat/config.rs:patch →UpdateFlags→ 副作用src-tauri/src/config/config.rs:四个 Draft 配置层src-tauri/src/enhance/mod.rs:配置变换管线src-tauri/src/core/mod.rs:核心子模块边界crates/*/Cargo.toml:本地 crate 职责src/pages/_navigation.tsx:页面与导航定义src/services/、src/hooks/、src/providers/:前端数据层