跳转到内容

Tauri 2 分层架构设计

Tauri 2 分层架构设计

学习目标

  • 理解 Tauri 2 应用的四层架构设计
  • 掌握 130+ Tauri 命令的组织方式与模块拆分策略
  • 了解 AppState 全局状态管理与插件集成模式
  • 识别 Tauri 应用的生命周期管理关键点

前置知识

需要了解 Rust 的基本概念(struct、trait、async/await)和 Tauri 框架的定位。


项目实践

四层架构

CC Switch 的 Rust 后端严格遵循四层分离:

命令层(src-tauri/src/commands/

  • 职责:接收来自前端的 Tauri invoke 调用,参数验证,调用服务层,返回结果
  • 文件组织:按功能域拆分(provider.rsproxy.rsmcp.rsskill.rs 等 30+ 文件)
  • 注册方式:在 lib.rsinvoke_handler 中统一注册(130+ 命令)
.invoke_handler(tauri::generate_handler![
commands::get_providers,
commands::add_provider,
commands::switch_provider,
// ... 130+ commands
])

设计决策:命令层只做参数接收和结果转发,不包含业务逻辑。这使得服务层可以被命令层和测试代码复用。

服务层(src-tauri/src/services/

  • 职责:业务逻辑实现(Provider CRUD、代理启动/停止、MCP 同步等)
  • 结构:每个服务是一个 Clone 的 struct,内部持有 Arc<Database> 或其他依赖
  • 并发:通过 Arc<Database> 共享数据库连接,避免多个服务实例创建多个连接
pub struct ProxyService {
db: Arc<Database>,
server: Arc<RwLock<Option<ProxyServer>>>,
app_handle: Arc<RwLock<Option<tauri::AppHandle>>>,
switch_locks: SwitchLockManager,
}

数据访问层(src-tauri/src/database/dao/

  • 职责:SQLite SQL 语句封装,不涉及业务逻辑
  • 组织:按表拆分(providers.rsmcp.rsprompts.rsskills.rssettings.rs
  • 连接安全:通过 lock_conn! 宏获取 Mutex 保护,panic 时转换为 AppError 而非 crash

数据库(src-tauri/src/database/mod.rs

  • Database struct 封装 Mutex<Connection>
  • init() 负责文件打开、PRAGMA 设置、表创建、Schema 迁移
  • memory() 创建内存数据库用于测试

全局状态管理

CC Switch 使用 Tauri 的 app.manage() 注册全局状态:

// 核心状态
app.manage(app_state); // AppState { db, proxy_service }
app.manage(SkillServiceState(Arc::new(skill_service)));
app.manage(CopilotAuthState(Arc::new(RwLock::new(auth_manager))));
app.manage(CodexOAuthState(Arc::new(RwLock::new(oauth_manager))));

命令层通过 tauri::State 获取这些单例:

#[tauri::command]
async fn get_providers(
state: tauri::State<'_, AppState>,
app_type: String,
) -> Result<HashMap<String, Provider>, String> {
state.db.get_all_providers(&app_type)
}

为什么用 tauri::State 而非全局 static:Tauri 的状态管理保证每个状态只有一个实例,且在应用退出时自动清理。全局 static 需要手动管理生命周期,且在 Tauri 的多窗口场景下可能出现不一致。

插件集成

CC Switch 集成了多个 Tauri 插件:

插件用途
tauri-plugin-log日志输出(单文件 + stdout)
tauri-plugin-dialog文件选择对话框
tauri-plugin-store持久化键值存储(配置目录覆盖)
tauri-plugin-updater自动更新
tauri-plugin-deep-linkDeep Link URL 处理
tauri-plugin-window-state窗口位置/大小持久化
tauri-plugin-single-instance防止多实例运行
tauri-plugin-process进程管理
tauri-plugin-opener打开外部链接/文件

插件注册顺序很重要:Store 插件必须在日志插件之前注册,因为日志目录可能由 Store 覆盖的配置决定。

应用生命周期

关键生命周期钩子

  • setup() — 应用初始化,创建数据库、State、托盘、注册 deep-link
  • on_window_event — 窗口关闭时决定最小化到托盘还是真正退出
  • ExitRequested — 用户退出时清理代理、保存窗口状态
  • Reopen(macOS)— Dock 图标点击时恢复主窗口

问题与规避

陷阱 1:130+ 命令的注册维护困难

问题:所有命令在 lib.rs 的一个 generate_handler! 宏中注册,新增命令容易忘记注册。

CC Switch 的解决方案

  • 命令按功能域组织在 commands/ 子目录中,每个子模块有 mod.rs 导出
  • 部分命令通过 commands::* 通配符导入,减少手动维护
  • 编译时如果命令函数存在但未注册,不会报错,所以代码审查时需要关注

陷阱 2:Mutex 死锁

问题:多个服务同时操作数据库,如果获取锁的顺序不一致,可能死锁。

CC Switch 的解决方案

  • 所有数据库操作通过同一个 Database struct 的 lock_conn! 宏获取锁
  • 服务层不直接获取底层 Connection 的锁
  • SwitchLockManager 为 per-app 操作提供独立的 Mutex,避免代理切换与配置写入竞争

陷阱 3:Rust 1.85 版本要求

问题:CC Switch 要求 Rust 1.85+(通过 rust-toolchain.toml 指定),开发者可能使用旧版本。

规避:CI 中通过 rust-toolchain.toml 自动安装正确版本,本地开发通过 rustup 自动同步。


设计取舍

为什么选择 Tauri 2 而非 Electron?

维度Tauri 2Electron
安装包大小~15MB(Rust 编译 + 系统 WebView)~150MB(捆绑 Chromium)
内存占用~50MB 基础~200MB 基础
安全性Rust 内存安全 + 系统 WebView 沙箱Chromium 沙箱
开发体验需要 Rust 知识,前端部分相同纯 JS/TS,生态更成熟
WebView 差异依赖系统 WebView(macOS=WebKit, Windows=WebView2, Linux=WebKitGTK)统一 Chromium

对于 CC Switch 这种配置管理工具(不需要复杂的渲染特性),Tauri 2 的小体积和低内存是明显优势。


参考来源