Skip to content

第 14 章:托盘、窗口、轻量模式与后台任务

桌面客户端即使关闭主窗口也常要继续代理。托盘、热键、窗口和后台任务因此不是附属功能,而是另一套产品入口。本章解释它们怎样共享 Config/RunState,又如何避免把平台差异带进 React。

一、托盘是第二套 UI

Tray 不只包含“显示/退出”,还提供:

  • 当前代理模式;
  • 系统代理和 TUN 开关;
  • Proxy group/node 选择;
  • Profile 切换;
  • lightweight 状态;
  • 更新提示;
  • 动态图标、tooltip 和 macOS 速度文本。

因此配置或节点改变后,既要刷新 WebView,也要更新 tray。feat/config.rs::UpdateFlags 把 menu/icon/tooltip 分开,避免任何 patch 都重建全部托盘资源。

二、托盘状态必须读取同一权威快照

Tray 通过 Config、RunState 和 Mihomo API构建菜单,不维护独立业务状态。节点切换后:

  1. 切 Mihomo group;
  2. 记录 Profile selection;
  3. 发前端 proxy refresh;
  4. 更新 tray menu。

若 tray 自己保存“当前节点”,核心 reload 后就会与实际状态分裂。

三、热键映射为产品动作

Hotkey 模块负责注册/更新 global shortcut,并将动作路由到:

  • 切系统代理;
  • 切 TUN;
  • 切模式;
  • 显示窗口;
  • 执行其他配置 patch。

热键不应直接修改 YAML。它仍调用与设置页相同的 feat,因此校验、RunState reconciliation、tray refresh 都不会被绕过。

窗口 focused/unfocused 时,macOS 还动态处理 Cmd+Q/Cmd+W 与用户 global hotkey 设置,避免系统快捷键和自定义注册互相冲突。

四、窗口状态与核心状态分离

关闭主窗口通常执行 hide,不退出应用。WindowManager 统一处理:

  • create/show/hide/destroy;
  • 装饰与自定义 titlebar;
  • 尺寸位置恢复;
  • 多平台 focus/activation policy;
  • macOS WebContent process 终止后的 reload。

前端 WindowProvider 只暴露装饰和窗口操作,不直接接触平台 API 分支。

五、轻量模式为什么直接销毁窗口

普通隐藏仍保留 WebView、React、Canvas、WebSocket 和 JS heap。轻量模式目标是减少后台资源,因此:

  • 状态从 Normal → In;
  • 销毁主窗口;
  • 核心、Service、tray 和定时器继续;
  • 用户重新打开时进入 Exiting,重建窗口,最后回 Normal。

状态用 AtomicU8 CAS,保证并发入口只有一个成功。退出轻量模式在窗口重建完成后才回 Normal,防止第二个 show 与第一个交错。

六、自动轻量模式的可取消定时器

启用后监听:

  • window close requested → 启动倒计时;
  • WebView focus → 取消倒计时。

Timer 使用 oneshot sender 存在全局 Mutex:

  • 已有 sender 表示 timer 已存在,不重复创建;
  • cancel 取出 sender 并发送;
  • timeout 先清 sender,再进入 lightweight。

这种“单槽取消句柄”比多个 sleep 后再检查 bool 更节省任务,也更容易保证只触发一次。

七、静默启动是窗口策略,不是核心策略

enable_silent_start 使 init_window() 创建但不显示窗口,并可在 auto_lightweight_boot() 中直接进入轻量模式。核心、tray、backup、updater 仍正常初始化。

因此不要在“窗口不可见”时停止数据面。窗口只是管理界面,代理核心是后台服务能力。

八、自动备份管理器

AutoBackupManager 支持:

  • 固定小时周期;
  • Global Merge 变化触发;
  • Global Script 变化触发;
  • 至少 60 秒间隔去抖;
  • 执行 Mutex 防并发;
  • 自动备份最多保留 20 份。

设置通过 watch::Sender 推给 scheduler:

text
schedule disabled → 等待 settings changed
schedule enabled  → sleep(interval) 与 settings changed 竞争

修改间隔不必杀死/重建后台线程,watch change 会打断当前 sleep 并按新值重新开始。

九、备份为什么要加密敏感字段

WebDAV 配置含用户名/密码。config/encrypt.rs 使用 AES-256-GCM:

  • 每次随机 12 字节 nonce;
  • nonce + ciphertext 做 base64;
  • 加密 key 来自应用目录/平台派生;
  • task-local ENCRYPTION_ACTIVE 控制序列化时是否加密。

同一个 struct 在普通内存/YAML 操作时保持原类型,只有备份序列化作用域启用加密 serializer。task-local 能跨 async await 跟随当前任务,又不会影响并发的普通保存。

十、静默更新器

SilentUpdater 管理:

  • 后台检查;
  • 下载 bytes;
  • 内存 pending update;
  • 磁盘 cache 的 binary 与 metadata;
  • 下次启动恢复 pending;
  • 安装前停止核心并清系统状态。

Tauri updater 配置包含多个镜像 endpoint 和 minisign public key。镜像提高可用性,签名验证保证下载源替换不等于更新信任被替换。

缓存元数据记录 version/downloaded_at。安装前仍要与当前版本和 update metadata 对齐,不能只因为本地存在 pending_update.bin 就执行。

注册 clash://clash-verge:// 后:

  • 主实例在 Tauri deep-link event 中解析;
  • 第二实例通过 loopback singleton server 把 URL 交给主实例;
  • resolve::scheme 判断是订阅导入还是其他动作;
  • WindowManager 必要时显示主窗口。

Deep link 解析属于后端,因为它会触发 Profile 创建、文件写入和窗口操作,不应只是 React Router URL。

十二、跨平台差异的典型收敛点

能力WindowsmacOSLinux
服务Windows Servicelaunch daemon/helpersystemd
进程收束Job Object进程/Service进程/Service
deep linkRegistrybundle schemexdg-mime
代理sysproxy/本地Service 或本地sysproxy/本地
窗口自定义 controlsactivation policyWebKit/Wayland workaround

上层通过同名动作或 RunState 观察结果,不把平台矩阵复制到每个页面。

十三、桌面后台能力的设计原则

  1. 托盘与热键是 UI 入口,但不是独立状态源。
  2. 窗口生命周期与核心生命周期解耦。
  3. 后台 timer 必须可取消、去重并对配置热更新。
  4. 敏感数据只在明确序列化作用域加密。
  5. 更新可用性与更新真实性分开:镜像负责前者,签名负责后者。

本章源码索引

  • src-tauri/src/core/tray/:菜单、图标与速度任务
  • src-tauri/src/core/hotkey.rs:全局热键
  • src-tauri/src/utils/window_manager.rsfeat/window.rs:窗口生命周期
  • src/providers/window/:前端窗口抽象
  • src-tauri/src/module/lightweight.rs:轻量模式状态机与 timer
  • src-tauri/src/module/auto_backup.rs:watch scheduler 与保留策略
  • src-tauri/src/feat/backup.rscore/backup.rs:本地/WebDAV 备份
  • src-tauri/src/config/encrypt.rs:AES-GCM task-local 序列化
  • src-tauri/src/core/updater.rs:静默下载、缓存与安装
  • src-tauri/src/utils/resolve/scheme.rs:Deep Link 分派

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