第 1 章:为什么 Clash Verge Rev 值得拆解
本章不急着进入某个函数。先回答三个问题:它到底是什么,复杂度来自哪里,以及应该沿哪条主线阅读。读完后,你应该能用一张图解释“点击一个开关,为什么会牵动配置、进程、操作系统和界面”。
一、它不是 Mihomo 的“皮肤”
最容易产生的误解,是把 Clash Verge Rev 看成 Mihomo 的 GUI。GUI 确实是它的一部分,但生产级桌面客户端还必须承担 Mihomo 本身不负责的工作:
- 管理远程订阅、本地配置和多个增强片段;
- 把用户意图编译成可运行、可校验的 YAML;
- 决定 Mihomo 由特权服务还是普通子进程托管;
- 修改系统代理、TUN、DNS、自启动和协议处理器;
- 在核心重启、服务损坏、端口冲突时恢复到一致状态;
- 将高频流量、连接、日志和节点状态送进 React;
- 为 Windows、macOS、Linux 打包、更新和分发。
因此更准确的定义是:
Clash Verge Rev 是一个围绕 Mihomo 构建的跨平台控制面。它负责编译配置、管理核心进程、协调系统网络状态,并将这些能力通过 Tauri 暴露给 React 产品界面。
二、三个身份
2.1 配置编译器
输入并不是一个 YAML,而是一组具有优先级和所有权的材料:
| 输入 | 代表什么 |
|---|---|
| 当前 Profile | 机场订阅或本地基础配置 |
| Application Merge Config | 应用维护的端口、控制器、模式等设置 |
| Rules / Proxies / Groups | 可追加、前置、删除的序列增强项 |
| Global Merge / Script | 对全部 Profile 生效的用户覆盖 |
| Profile Merge / Script | 只对当前 Profile 生效的覆盖 |
| TUN / DNS / 内建脚本 | 应用根据开关生成的派生配置 |
这些材料经过固定顺序处理,输出 clash-verge.yaml 一类运行时文件。这个过程更像编译:有源输入、有变换阶段、有不可被覆盖的控制面、有校验,也有失败回退。
2.2 进程监督器
Mihomo 有两条运行路径:
- Service:由独立特权服务托管,适合 TUN 等需要权限的能力;
- Sidecar:由桌面应用直接拉起子进程,不依赖已安装服务。
桌面进程需要判断服务是否存在、版本是否兼容、IPC 是否可达、当前所有者是否仍有效,并在两种模式之间安全交接。RunningMode 只是表面,真正的状态还包括服务健康、待执行操作、是否允许本次会话降级到 Sidecar、是否正在执行特权操作等。
2.3 实时数据客户端
界面不是简单地“请求一次然后展示”。连接、日志、上下行流量都持续变化:
- Tauri commands 处理低频请求/响应;
- Tauri events 推送配置和运行状态变化;
- Mihomo WebSocket 承载高频流;
- SWR 负责请求缓存与失效;
useSyncExternalStore和共享订阅表避免重复连接;- Web Worker 对流量序列做采样和压缩。
这使前端同时具有传统桌面配置页和实时监控面板两种数据模型。
三、先看规模,再看边界
本地快照的有效源码规模如下(排除前端自动生成 i18n 文件):
数字告诉我们两件事:
- 这不是一个“Rust 后端很薄、主要逻辑在 React”的项目。后端核心模块约 1.48 万行,是系统复杂度中心。
- 也不是一个“Rust 完成一切、前端只渲染”的项目。组件目录接近 3 万行,包含虚拟列表、画布曲线、编辑器和复杂交互。
四、全系统主线
阅读时始终记住下面这条闭环:
用户修改设置或选择 Profile
↓
React 调用 src/services/cmds.ts
↓ invoke
Rust cmd 层把参数交给 feat/config/core
↓
Draft 中暂存修改
↓
enhance() 生成候选 Runtime Mapping
↓
写检查文件 → Mihomo 校验
↓ 成功
提交 Draft + 写正式文件 + reload/restart core
↓
Handle 发 Tauri event
↓
SWR mutate / Provider 更新 / 页面重渲染
↓
Mihomo WebSocket 继续返回实时运行数据这条链路比目录结构更重要。很多设计只有放回闭环才能理解:
- Draft 为什么既有
latest_arc()又有data_arc()?因为副作用发生前要看候选值,提交后持久化必须看正式值。 - 为什么配置修改不能都调用同一个 API?因为有的字段可热更新,有的必须重启核心,有的还要重设系统代理。
- 为什么系统代理恢复需要 generation?因为旧的异步任务可能在核心所有权已经变化后才返回。
- 为什么前端事件订阅完成后还要重新读取?因为“先读、后订阅”之间存在丢事件窗口。
五、复杂度的四个来源
5.1 多个真相源
“端口是多少”至少有三种答案:用户选择的、合并配置计算出的、当前核心实际监听的。MixedPort::desired() 和 MixedPort::effective() 刻意区分“期望状态”和“运行状态”。
5.2 长事务跨越内存、文件和外部进程
修改端口时,系统可能需要:暂存三层配置、生成 runtime、校验、写多个文件、重启核心、恢复系统代理。Rust 的内存事务无法自动回滚文件和操作系统状态,项目因此同时使用 DraftTransaction、文件快照和显式生命周期补偿。
5.3 特权边界
Service 不是另一种启动参数,而是安全与所有权边界。系统必须证明“当前桌面进程仍然拥有这次服务会话”,才能执行系统代理、停止核心或写入配置。
5.4 高频与低频数据共存
配置更新以秒或分钟计,流量以几十毫秒计。若把它们都塞进 React Context 或都用轮询处理,性能和一致性都会恶化。因此项目同时使用 command、event、WebSocket、SWR、外部 store 和 Worker。
六、本教程的拆解方式
本教程参考“是什么 → 怎么做 → 为什么”的结构,但会针对桌面网络客户端增加两个维度:
- 失败时发生什么:启动、写文件、校验、服务 IPC、代理恢复都必须讨论失败路径。
- 谁拥有这个状态:区分用户配置、应用派生状态、核心实况和操作系统状态。
每章最后提供源码索引。建议在阅读正文后打开对应文件,先找入口符号,再顺着调用链阅读,而不是从文件第一行线性读到最后。
七、可以带走的三个判断
- Clash Verge Rev 的核心产品不是页面,而是控制面闭环。
- 最重要的抽象不是某个组件,而是配置编译、运行状态和所有权证明。
- 项目的工程价值主要体现在失败路径。 正常路径“生成配置并启动核心”并不难,难的是端口占用、服务不兼容、旧任务晚到、写入一半失败和退出时恢复系统状态。
本章源码索引
package.json:前端技术栈、脚本与版本基线Cargo.toml:Rust workspace 与共享依赖src-tauri/src/config/config.rs:配置聚合入口Configsrc-tauri/src/enhance/mod.rs:配置编译主函数enhance()src-tauri/src/core/manager/mod.rs:核心监督器CoreManagersrc-tauri/src/core/runstate/health.rs:统一运行状态RunStatesrc/services/cmds.ts:前端到 Tauri 的命令门面src/hooks/use-mihomo-ws-subscription.ts:共享 WebSocket 订阅