Skip to content

第 2 章:工程骨架与依赖方向

目录很多并不可怕,危险的是把目录名当成架构。本章从依赖方向出发,将项目划分为产品界面、IPC 门面、领域编排、基础设施与独立 crate,解释每一层可以知道什么、不应该知道什么。

一、顶层不是前后端二分

仓库顶层看起来像典型 Tauri 项目:

text
clash-verge-rev-dev/
├── src/                 React + TypeScript
├── src-tauri/           Tauri 应用 crate
├── crates/              可复用 Rust crates
├── scripts/             开发、打包、更新脚本
├── template/            发布模板
├── tests/               前端集成测试
└── .github/workflows/   多平台 CI/CD

但后端内部不是一个平铺的“API 层”,而是至少五种职责:

text
cmd → feat → config / enhance / core → utils / plugins / OS

前端也不是简单的 pages → components → api

text
pages → components → hooks → services → Tauri/Mihomo
                    ↘ providers / external stores / workers

二、Rust 主应用的职责层

2.1 cmd/:IPC 适配层

cmd 中的函数由 tauri::generate_handler! 注册。它应当负责:

  • 接收可序列化参数;
  • 调用领域函数;
  • 将错误转换为前端可接收的字符串;
  • 不持有复杂业务状态。

例如 Profile command 不应该自己实现下载重试,而是把工作交给 feat::profileconfig::profiles

2.2 feat/:用例编排层

feat 对应用户可感知的动作:打补丁、切 Profile、更新订阅、切节点、退出应用、保存监听端口。它连接多个内部模块,决定副作用顺序。

feat/config.rs 是典型例子。一个 IVerge patch 会先被转成 UpdateFlags,再选择性执行:

  • restart core;
  • reload config;
  • 更新自启动;
  • 重新应用系统代理;
  • 更新热键、托盘、语言、日志和轻量模式。

这里的关键不是 bitflags 本身,而是把“字段变化”翻译成“必须执行的副作用集合”。

2.3 config/:持久状态与候选状态

Config 聚合四类配置:

字段类型角色
clash_configDraft<IClashTemp>应用维护的 Mihomo 合并配置
verge_configDraft<IVerge>桌面应用自身设置
profiles_configDraft<IProfiles>Profile 索引、当前项和增强项
runtime_configDraft<IRuntime>增强流水线的产物

它既是状态仓库,也是配置生成入口,但没有直接处理页面。

2.4 enhance/:纯变换优先的编译管线

enhance() 收集 Profile 与应用设置,然后按严格顺序变换 serde_yaml_ng::Mapping。内部函数尽量接收参数并返回新 Mapping,例如:

  • use_seq
  • merge_default_config
  • use_merge
  • use_script
  • use_tun
  • cleanup_proxy_groups
  • use_sort

这让复杂的优先级可以通过单元测试固定,而不是隐藏在全局状态和 I/O 中。

2.5 core/:外部世界的协调器

core 负责所有“不是改一个内存对象就能完成”的工作:

  • 启停 Mihomo;
  • 服务安装、探测和所有权监控;
  • 运行状态机;
  • 系统代理和 TUN 能力;
  • 配置验证与热加载;
  • 托盘、热键、定时器、日志、更新器;
  • 向前端发事件。

它是后端最大区域,也是并发与故障恢复最密集的区域。

2.6 utils/:平台和基础设施

这里包含目录解析、嵌入 HTTP 服务器、单例检测、网络探测、窗口管理、DNS 恢复、Windows 计划任务、Linux MIME 等。一个实用判断是:

如果代码回答“在某个平台上怎样做”,它多半属于 utils;如果回答“产品在何时做”,它属于 featcore

三、独立 crates:把稳定机制从产品壳中抽出

workspace 包含六个本地 crate:

crate核心职责为什么独立
clash-verge-draftCOW 快照、草稿、事务配置一致性的通用机制
clash-verge-logging日志类型、宏、过滤和 sidecar writer避免主 crate 到处重复格式
clash-verge-signalUnix/Windows 退出信号和 latch平台差异清晰、可测试
clash-verge-i18nRust 侧语言解析和翻译托盘/通知不依赖前端 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(),上层只面向统一动作。并非所有文件都做到了这一点,但整体方向清晰。

七、判断架构是否健康的三个问题

  1. 一个新页面是否必须知道 Service IPC token?不应该,它只应看到 RunStateView
  2. 一个配置合并函数是否必须读取 Tauri AppHandle?通常不应该,它应接收参数并返回 Mapping。
  3. 一个系统代理实现是否可以自行决定何时运行?不应该,时机属于 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/:前端数据层

原创文档 CC BY-SA 4.0 · 站点代码 MIT