5 状态管理
原文链接: https://tauri.app/develop/state-management/
在 Tauri 应用中,你经常需要跟踪应用的当前状态,或者管理与应用相关联事物的生命周期。Tauri 通过 Manager API 提供了一种简单的方式来管理应用状态,并在命令被调用时读取它。
下面是一个简单示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| use tauri::{Builder, Manager};
struct AppData {
welcome_message: &'static str,
}
fn main() {
Builder::default()
.setup(|app| {
app.manage(AppData {
welcome_message: "Welcome to Tauri!",
});
Ok(())
})
.run(tauri::generate_context!())
.unwrap();
}
|
之后你可以用任何实现了 Manager trait 的类型访问状态,例如 App 实例:
1
| let data = app.state::<AppData>();
|
更多信息(包括在命令中访问状态)请参阅访问状态一节。
可变性
在 Rust 中,你不能直接修改在多个线程之间共享的值,或者通过 Arc(或 Tauri 的 State)这类共享指针控制所有权的值。这样做可能导致数据竞争(例如两次写入同时发生)。
要绕过这一点,你可以使用一种称为内部可变性的概念。例如,可以用标准库的 Mutex 包裹你的状态。这样你就可以在需要修改时锁定该值,完成后解锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
use tauri::{Builder, Manager};
#[derive(Default)]
struct AppState {
counter: u32,
}
fn main() {
Builder::default()
.setup(|app| {
app.manage(Mutex::new(AppState::default()));
Ok(())
})
.run(tauri::generate_context!())
.unwrap();
}
|
现在可以通过锁定互斥锁来修改状态:
1
2
3
4
5
6
|
// 锁定互斥锁以获得可变访问:
let mut state = state.lock().unwrap();
// 修改状态:
state.counter += 1;
|
在作用域结束时,或者 MutexGuard 被以其它方式丢弃时,互斥锁会自动解锁,以便应用中的其它部分访问并修改其中的数据。
何时使用异步互斥锁
引用 Tokio 文档的说法,通常使用标准库的 Mutex 而不是 Tokio 提供的异步互斥锁是没问题的:
与流行的看法相反,在异步代码中使用标准库的普通 Mutex 是可以的,而且往往更受推荐……异步互斥锁的主要用例是为数据库连接这类 IO 资源提供共享可变访问。
完整阅读链接的文档以理解两者之间的取舍是个好主意。你_确实_需要异步互斥锁的一种情形是:你需要在 await 点之间持有 MutexGuard。
你需要 Arc 吗?
在 Rust 中经常看到用 Arc 在多个线程间共享值的所有权(通常与 Mutex 搭配成 Arc<Mutex<T>>)。不过,对于存放在 State 中的东西,你不需要使用 Arc,因为 Tauri 会替你处理。
如果 State 的生命周期要求使你无法把状态移动到新线程中,你可以改为把 AppHandle 移动到该线程,然后按下文“使用 Manager trait 访问状态”一节所示取出状态。AppHandle 的克隆开销被刻意设计得很低,正适合这种用例。
访问状态
在命令中访问状态
1
2
3
4
5
| fn increase_counter(state: State<'_, Mutex<AppState>>) -> u32 {
let mut state = state.lock().unwrap();
state.counter += 1;
state.counter
}
|
关于命令的更多信息,请参阅从前端调用 Rust。
异步命令
如果你使用 async 命令并想使用 Tokio 的异步 Mutex,可以用同样的方式设置,并这样访问状态:
1
2
3
4
5
| async fn increase_counter(state: State<'_, Mutex<AppState>>) -> Result<u32, ()> {
let mut state = state.lock().await;
state.counter += 1;
Ok(state.counter)
}
|
注意如果你使用异步命令,返回类型必须是 Result。
有时你可能需要在命令之外访问状态,例如在另一个线程中,或在 on_window_event 这类事件处理函数中。这种情况下,你可以使用实现了 Manager trait 的类型(例如 AppHandle)的 state() 方法获取状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| use tauri::{Builder, Window, WindowEvent, Manager};
#[derive(Default)]
struct AppState {
counter: u32,
}
// 在事件处理函数中:
fn on_window_event(window: &Window, _event: &WindowEvent) {
// 获取 app 的句柄,以便取得全局状态。
let app_handle = window.app_handle();
let state = app_handle.state::<Mutex<AppState>>();
// 锁定互斥锁以可变地访问状态。
let mut state = state.lock().unwrap();
state.counter += 1;
}
fn main() {
Builder::default()
.setup(|app| {
app.manage(Mutex::new(AppState::default()));
Ok(())
})
.on_window_event(on_window_event)
.run(tauri::generate_context!())
.unwrap();
}
|
当你无法依赖命令注入时,这个方法很有用。例如,当你需要把状态移动到使用 AppHandle 更方便的线程中,或者你不在命令上下文中时。
类型不匹配
警告
如果你为 State 参数使用了错误的类型,运行时会发生 panic,而不是编译期报错。
例如,如果你使用 State<'_, AppState> 而不是 State<'_, Mutex<AppState>>,就不会有任何以该类型托管的状态。
如果你愿意,可以用类型别名包裹状态以避免这个错误:
1
2
3
4
5
6
7
|
#[derive(Default)]
struct AppStateInner {
counter: u32,
}
type AppState = Mutex<AppStateInner>;
|
不过,请确保按原样使用类型别名,不要再把它包一层 Mutex,否则你会遇到同样的问题。