Skip to content

第 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 文件):

43,860前端 TS/TSX 行
35,880Rust 行
89Tauri commands
8产品页面
13前端语言目录
365Rust 测试标记

数字告诉我们两件事:

  1. 这不是一个“Rust 后端很薄、主要逻辑在 React”的项目。后端核心模块约 1.48 万行,是系统复杂度中心。
  2. 也不是一个“Rust 完成一切、前端只渲染”的项目。组件目录接近 3 万行,包含虚拟列表、画布曲线、编辑器和复杂交互。

四、全系统主线

阅读时始终记住下面这条闭环:

text
用户修改设置或选择 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。

六、本教程的拆解方式

本教程参考“是什么 → 怎么做 → 为什么”的结构,但会针对桌面网络客户端增加两个维度:

  1. 失败时发生什么:启动、写文件、校验、服务 IPC、代理恢复都必须讨论失败路径。
  2. 谁拥有这个状态:区分用户配置、应用派生状态、核心实况和操作系统状态。

每章最后提供源码索引。建议在阅读正文后打开对应文件,先找入口符号,再顺着调用链阅读,而不是从文件第一行线性读到最后。

七、可以带走的三个判断

  1. Clash Verge Rev 的核心产品不是页面,而是控制面闭环。
  2. 最重要的抽象不是某个组件,而是配置编译、运行状态和所有权证明。
  3. 项目的工程价值主要体现在失败路径。 正常路径“生成配置并启动核心”并不难,难的是端口占用、服务不兼容、旧任务晚到、写入一半失败和退出时恢复系统状态。

本章源码索引

  • package.json:前端技术栈、脚本与版本基线
  • Cargo.toml:Rust workspace 与共享依赖
  • src-tauri/src/config/config.rs:配置聚合入口 Config
  • src-tauri/src/enhance/mod.rs:配置编译主函数 enhance()
  • src-tauri/src/core/manager/mod.rs:核心监督器 CoreManager
  • src-tauri/src/core/runstate/health.rs:统一运行状态 RunState
  • src/services/cmds.ts:前端到 Tauri 的命令门面
  • src/hooks/use-mihomo-ws-subscription.ts:共享 WebSocket 订阅

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