全局状态:tauri::State 与共享数据

应用总有一份全局数据:用户配置、登录态、数据库连接、缓存。在 Tauri 中,这类状态最自然的位置是 Rust 核心进程,通过 tauri::State 在命令里共享访问。本篇讲 State 的基本用法、可变状态与线程安全、以及与前端状态管理的配合。

一、State 的基本用法

State 是一个由 Tauri 管理的依赖注入容器。先在 Builder 上注册,之后任何命令都能按类型拿到:

use std::sync::Mutex;
use tauri::State;

struct AppState {
    counter: Mutex<i32>,
}

#[tauri::command]
fn get_count(state: State<'_, AppState>) -> i32 {
    *state.counter.lock().unwrap()
}

fn main() {
    tauri::Builder::default()
        .manage(AppState { counter: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![get_count])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

manage 注册的类型必须是 Send 加 Sync,因为状态会被多个命令并发访问。命令参数里 State<'_, T> 由框架自动注入,前端无需传参。

对比项 前端状态(如 Pinia) Rust State
存储位置 WebView 内存 核心进程内存
跨窗口共享 需要手动同步 天然共享
持久化 localStorage 可直接写文件或数据库
计算性能 受 JS 限制 Rust 原生速度

二、可变状态与锁

Rust 的借用规则决定了可变共享必须用锁。常用 Mutex 保护内部可变数据,复杂场景用 RwLock:

use std::sync::Mutex;

#[tauri::command]
fn increment(state: State<'_, AppState>) -> i32 {
    let mut count = state.counter.lock().unwrap();
    *count += 1;
    *count
}

锁的粒度有讲究:粒度太粗影响并发,太细容易死锁。跨 await 持锁是异步 Rust 的经典陷阱,命令里如果同时持有锁并 await,应当先 clone 出数据再释放锁。线程安全的系统知识见「所有权与线程安全:Send、Sync 与 Mutex」。

三、状态的初始化时机

复杂状态初始化可能依赖配置文件或网络。常用做法是在 setup 钩子里完成初始化后再 manage:

use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let config = load_config().unwrap_or_default();
            app.manage(AppState { config });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

setup 里可以通过 app.manage 注册,效果与 Builder 上相同,好处是能访问应用上下文。数据库连接这类重量级资源也建议在这里统一创建,具体见「数据库:SQL 插件与 SQLite 实战」。

四、与前端状态管理配合

大方向是:UI 临时状态留给前端,业务核心状态放 Rust。判断标准是这条数据是否需要跨窗口一致、是否涉及系统能力、是否需要在后台任务中访问。

状态类型 建议位置
表单输入、临时开关 前端状态库
用户配置、登录态 Rust State 加持久化
数据库连接、句柄 只能放 Rust
大量计算结果 Rust,按需推给前端

前端想知道 Rust 状态变化,用事件订阅;前端操作状态,用命令。两套机制配合覆盖绝大多数场景,具体通信写法见「事件系统:Rust 主动通知前端」与「前端调用 Rust:invoke 与命令系统」。

五、进阶:多模块拆分状态

状态多了以后全塞一个结构体里难维护。可以按功能拆成多个结构体,分别 manage,命令按需注入。例如拆成 UserState、CacheState、DbState,各自持有自己的锁。Rust 的模块系统让拆分很自然,模块组织基础可以复习 Rust 入门。

生命周期方面,State 的存活时间与应用一致,应用退出即销毁,因此析构逻辑(比如关闭数据库)可以放在 Drop 实现里,类型系统会保证调用。

样板代码(结构体定义、锁包装、命令转发)重复度高,适合用 AI 生成初稿,参考 Claude Code 指南 的模式;处理多模块代码库时 正则 mastery 的批量重构技巧也很实用。

六、状态与持久化的边界

State 只解决内存共享,应用重启后一切归零。需要持久化时要决定持久化什么、什么时候写。三种常见取舍:

策略 写时机 风险
即时写 每次变更立即落盘 IO 频繁
防抖写 变更合并后批量写 崩溃丢少量数据
快照写 定时全量写 数据量较大时慢

对配置类数据,防抖写最常用:变更后延迟一秒合并写入,性能与可靠性平衡得最好。对数据库结构的数据,直接走 SQL 事务更合适,见「数据库:SQL 插件与 SQLite 实战」。无论哪种,都记得在应用退出前 flush 一次,给 WindowEvent::CloseRequested 或析构里留一次落盘机会。

状态管理看似简单,实则是应用架构的中枢。把「放哪里、谁来改、何时落盘」三个问题回答清楚,应用的可维护性就有了基本盘。

状态设计是应用架构的核心命题。Tauri 给你一个独特的机会:把状态的核心放在 Rust 侧,天然获得跨窗口一致性、并发安全与强类型。养成「先问 Rust 还是前端」的习惯,架构问题会少一半。下一章进入异步与后台任务,让状态真正流动起来。

最后给一个真实的取舍示例:用户登录态。放前端 Pinia 里,刷新即丢、多窗口不同步;放 Rust State 加持久化,所有窗口立刻共享且重启保留。但代价是每一次读取都要走 IPC。对登录态这种低频访问的数据,这个代价完全值得。决策的关键永远是「这个状态多久变一次、谁需要它、能否丢」,而不是抽象的哪个方案更先进。

状态管理没有银弹,Tauri 的优势是把「共享什么粒度」的选择权完整交给你。从一个小 State 开始随业务长大逐步拆分,是最自然的演进路径。写多了你会发现,状态划分清晰的应用调试与迭代都更轻快,这就是架构红利的体现。

状态设计最后再强调一次边界感:不是所有数据都值得做成共享状态,过度共享与过度分散一样是负担。一个简单的自检方法——问自己「这个状态同时被几个窗口或任务需要」。只有一个消费者时,它就只是局部变量或前端状态;两个以上才值得放进 Rust 共享层。这个朴素的标准能挡住大部分过度设计,让状态层保持精简而稳定。随着章节推进你会发现,命令、事件、状态三者拼在一起就是 Tauri 应用架构的全部基本盘。

下一章我们换换脑子,从「数据放哪里」转到「时间怎么花」——异步与后台任务是 Rust 在 Tauri 中真正发威的地方。状态加异步,是桌面应用骨架中两根最结实的骨头,两者配合好,应用怎么长大都不会变形。

延伸阅读

  • 所有权与线程安全:Send、Sync 与 Mutex——锁的原理与陷阱
  • 数据库:SQL 插件与 SQLite 实战——状态持久化的标准做法
  • Rust 入门:泛型与生命周期的前置知识