应用状态管理
本教程共 48 篇 · 第 25 篇 · 更新于 2026-08-09 · 约 7 分钟阅读
本节目标:学会用
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 本身、AppHandle、Window,都能通过 .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>,不是 AppData。State 是 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::RwLock,lock()改成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> 自动注入读取;可变状态必须用 Mutex 或 RwLock 包裹以保证多线程安全。Tauri 会自动用 Arc 包裹托管的状态,无需手动添加引用计数。
Tip你不需要自己套
Arc。Tauri 的State已经帮你用Arc包好,直接manage(你的实例)即可。官方文档也明确说:存进State的东西不必再加Arc。如果你的类型很长(比如Mutex<HashMap<String, String>>),可以起个别名type Store = Mutex<HashMap<String, String>>;,既好读又避免类型写错。