首页 / Tauri 2 入门教程 / 应用状态管理

Tauri 2 入门教程

应用状态管理

本教程共 48 篇 · 第 25 篇 · 更新于 2026-08-09 · 约 7 分钟阅读

状态管理statemanageMutexState

本节目标:学会用 app.manage 托管全局状态、用 State 在命令里读取它,并知道可变状态必须用 Mutex/RwLock 包裹才能安全跨线程共享。

什么是「应用状态」

写桌面程序时,你总会有些数据要在多处共用。比如当前登录的用户名、一个访问计数器、数据库连接池,或者一个放在内存里的键值表。这类「跟着应用一起存在、被多个命令反复读写的共享数据」,我们统称为应用状态(application state)。

在 Tauri 里,前端跑在 WebView 中,Rust 跑在后端进程。命令(command)是前后端之间的桥梁:前端调用命令,Rust 负责处理。可是命令彼此独立,怎么让它们共享同一份数据?答案就是把状态交给 Tauri 托管,任何命令需要时再取出来。

Tauri 借助 Rust 的 Manager trait 提供这套机制。它不引入新概念,而是复用你已有的 Rust 结构体:把你自己的 struct 用 app.manage(...) 注册进去,之后在命令参数里声明 State<你的结构体>,Tauri 就会自动把那份共享实例注入给你。

Note

这里说的「状态」是运行期内存里的数据,应用一关就消失。如果需要持久保存(重启后还在),要用 store 插件或自己读写文件,本教程不涉及这部分。

用 manage 把状态交给 Tauri 托管

最朴素的状态,就是一个只读的全局值。下面这个例子把一个欢迎语注册成应用状态:

use tauri::Manager;

struct AppData {
    welcome_message: &'static str,
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            app.manage(AppData {
                welcome_message: "欢迎使用 Tauri!",
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .unwrap();
}

关键点只有一处:在 setup 钩子里调用 app.manage(你的实例)setup 在应用启动时运行一次,非常适合做这种初始化。manage 接受你的结构体后,会把它放进 Tauri 内部的全局容器,并且自动用 Arc(原子引用计数)包好,所以你不需要自己写 Arc

之后任何实现了 Manager trait 的对象,比如 App 本身、AppHandleWindow,都能通过 .state::<T>() 把这份状态取回来。例如:

let data = app.state::<AppData>();
Tip

把状态注册放在 setup 里,能保证它在任何命令被调用之前就已就绪。不要在命令内部去 manage,那会得到重复注册的错误。

在命令里读取状态:State 注入

命令(command)是最常读状态的地方。Tauri 支持把状态当作命令参数直接注入,写法很自然:

#[tauri::command]
fn get_welcome(state: tauri::State<AppData>) -> &'static str {
    state.welcome_message
}

注意参数的类型是 tauri::State<AppData>,不是 AppDataState 是 Tauri 对共享实例的包装,它保证你拿到的一定是 manage 过的那个实例,并且生命周期安全。Tauri 在收到前端调用时,会自动从全局容器里找到 AppData 并填进 state 参数,你直接用即可。

记得把命令注册进 invoke_handler,前端才能调到:

.invoke_handler(tauri::generate_handler![get_welcome])
Warning

State 的类型必须和 manage 时完全一致。如果你 manage 的是 AppData,命令里却写成 State<OtherType>,编译能通过,但运行时会直接 panic,因为容器里找不到这个类型。这种「类型对不上」的错误只在运行期暴露,写的时候要特别留意。

可变状态要用 Mutex 包起来

前面的欢迎语是写死的常量,永远不变。但真实状态大多要改:计数器要加一、键值表要插入。Rust 有一条铁律——被多个线程共享的数据,不能直接随意改写,否则会出现数据竞争(data race),两个写操作同时发生时结果不可预测。

Tauri 的命令可能跑在不同线程上,所以共享状态必须是「线程安全的」。解决办法是用内部可变性(interior mutability),最常见的就是用标准库的 Mutex 把数据锁起来:

use std::sync::Mutex;
use tauri::Manager;

#[derive(Default)]
struct AppState {
    counter: u32,
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            app.manage(Mutex::new(AppState::default()));
            Ok(())
        })
        .run(tauri::generate_context!())
        .unwrap();
}

这里 manage 的不是 AppState 本身,而是 Mutex<AppState>Mutex 的中文是「互斥锁」:同一时刻只允许一个地方拿到锁、改数据,改完释放锁,别的地方才能进。这样就杜绝了并发写冲突。

在命令里改数据,就先 lock 再操作:

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

lock() 返回一个守卫(guard),离开作用域时自动解锁,所以你不用担心「忘了释放锁」。读也用同一把锁,保证读写互斥。

Note

如果你的场景是「读多写少」,可以用 RwLock(读写锁)代替 Mutex:多个读可以同时进行,只有写时才独占。语法几乎一样,把 Mutex 换成 std::sync::RwLocklock() 改成 write() 即可。需要跨 await 点持有锁时,才考虑 Tokio 的异步 Mutex

在非命令环境读取状态

有时你不在命令里,也想读状态。比如在窗口事件处理器、或者你新开的后台线程中。这时命令注入用不上,但 Manager trait 同样能帮你——只要手上有 AppHandle

use tauri::{Window, WindowEvent, Manager};

fn on_window_event(window: &Window, _event: &WindowEvent) {
    // 先拿到 AppHandle
    let app_handle = window.app_handle();
    // 再取出状态
    let state = app_handle.state::<Mutex<AppState>>();
    // 加锁后修改
    let mut state = state.lock().unwrap();
    state.counter += 1;
}

AppHandle 很轻量、克隆成本低,所以把它 move 进新线程也很常见:

let handle = app.handle().clone();
std::thread::spawn(move || {
    let state = handle.state::<Mutex<AppState>>();
    let counter = state.lock().unwrap().counter;
    println!("当前计数:{counter}");
});

小结

本章讲解了 Tauri 的应用状态管理机制:在 setup 钩子中用 app.manage(...) 将结构体注册为全局状态,命令通过参数 State<T> 自动注入读取;可变状态必须用 MutexRwLock 包裹以保证多线程安全。Tauri 会自动用 Arc 包裹托管的状态,无需手动添加引用计数。

Tip

你不需要自己套 Arc。Tauri 的 State 已经帮你用 Arc 包好,直接 manage(你的实例) 即可。官方文档也明确说:存进 State 的东西不必再加 Arc。如果你的类型很长(比如 Mutex<HashMap<String, String>>),可以起个别名 type Store = Mutex<HashMap<String, String>>;,既好读又避免类型写错。