使用 Tauri Plugin-Store 实现 Zustand 持久化与多窗口数据同步
在现代桌面应用程序开发中,状态管理和数据持久化是两个核心需求。特别是在使用 Tauri 构建桌面应用时,我们经常需要将应用状态持久化到本地存储,并在多窗口环境中保持数据同步。
本文将详细介绍如何结合 Tauri 的 plugin-store 和 Zustand 状态管理库来实现这一目标,包括完整的实现方案和使用示例。
为什么需要状态持久化和多窗口同步?
在桌面应用开发中,我们经常遇到以下需求:
- 状态持久化:应用关闭后重新打开时,用户设置和数据需要保持不变
- 多窗口同步:当应用有多个窗口时,各窗口间的数据需要保持同步
- 高性能:状态管理需要高效,避免不必要的性能损耗
技术选型:为什么选择 Zustand 和 Tauri Plugin-Store?
Zustand 状态管理库
Zustand 是一个轻量级、易于使用的状态管理库,特别适合 React 应用。它具有以下优势:
- 简单直观的 API
- 良好的 TypeScript 支持
- 无需额外的包装器或提供者组件
- 高性能的状态更新机制
Tauri Plugin-Store 插件
Tauri Plugin-Store 是 Tauri 生态系统中的官方插件,提供了键值对持久化存储功能。相比浏览器的 localStorage,它具有以下特点:
- 更安全、更可靠的持久化机制
- 跨窗口共享数据的能力
- 更好的性能表现
- 与 Tauri 应用深度集成
将这两者结合使用,我们可以实现:
- 应用状态的自动持久化
- 多窗口间的数据同步
- 高性能的状态管理
核心实现思路
我们的实现方案基于以下核心组件:
- Tauri Plugin-Store:提供底层的持久化存储功能
- Zustand:提供应用状态管理
- 自定义的 TauriStoreState 类:连接 Tauri Store 和 Zustand
- 自定义的 storeSave 命令:实现多窗口数据同步
下面我们将逐步实现这些组件。
实现步骤
1. 创建 TauriStoreState 类
首先,我们需要创建一个实现了 Zustand StateStorage
接口的类,用于连接 Tauri Store 和 Zustand:
typescript
// src/store/tauriStoreState.ts
import { getCurrentWindow, Window } from "@tauri-apps/api/window";
import { load, type Store } from "@tauri-apps/plugin-store";
import { debounce } from "lodash";
import type { StateStorage } from "zustand/middleware";
export type Options = {
// 数据重新加载时的回调函数
onRehydrate?: () => void;
// 防抖延迟时间(毫秒)
debounce?: number;
// 自定义保存函数,用于多窗口同步
saveFn?: (
currentWindow: Window,
storeName: string,
store: Store,
) => Promise<void>;
};
export class TauriStoreState implements StateStorage {
private store: Store | null = null;
private debouncedSave: (() => void) | null = null;
constructor(
public storename: string,
public options?: Options,
) {}
// 初始化存储
async init() {
// 加载指定名称的存储
this.store = await load(this.storename);
const window = getCurrentWindow();
// 监听数据重新加载事件
if (this.options?.onRehydrate) {
window.listen("rehydrate", this.options.onRehydrate);
}
// 创建保存函数,支持自定义保存逻辑
const saveFn = () => {
this.options?.saveFn
? this.options?.saveFn(window, this.storename, this.store!)
: this.store?.save();
};
// 使用防抖优化性能
this.debouncedSave = this.options?.debounce
? debounce(saveFn, this.options.debounce)
: saveFn;
}
// 获取存储项
async getItem(name: string) {
const res = await this.store?.get<string>(name);
return res || null;
}
// 设置存储项
async setItem(name: string, value: string) {
await this.store?.set(name, value);
this.debouncedSave?.();
}
// 删除存储项
async removeItem(name: string) {
await this.store?.delete(name);
this.debouncedSave?.();
}
}
这个类的关键特性包括:
- 实现了 Zustand 的
StateStorage
接口,包含getItem
、setItem
和removeItem
方法 - 使用
load
函数加载指定名称的 Store - 通过
getCurrentWindow
获取当前窗口,并监听 "rehydrate" 事件 - 使用防抖技术优化存储性能,避免频繁写入
- 支持自定义的
saveFn
回调函数,用于实现多窗口同步
2. 创建 Zustand Store
接下来,我们创建一个使用 TauriStoreState 的 Zustand store:
typescript
// src/store/useAppStore.ts
import { combine, createJSONStorage, persist } from "zustand/middleware";
import { shallow } from "zustand/shallow";
import { createWithEqualityFn } from "zustand/traditional";
import { storeSave } from "@/command";
import { TauriStoreState } from "./tauriStoreState";
// 创建存储实例
const store = new TauriStoreState("app-store.json", {
// 数据重新加载时的回调
onRehydrate: () => {
useAppStore.persist.rehydrate();
},
// 自定义保存函数,实现多窗口同步
saveFn: async (currentWindow, storeName) => {
await storeSave(storeName, currentWindow.label);
},
});
// 初始化存储
await store.init();
// 定义状态类型
export type Theme = "light" | "dark" | "system";
export type Language = "en" | "zh";
// 创建 Zustand store
export const useAppStore = createWithEqualityFn(
persist(
combine(
{
accessToken: "",
theme: "system" as Theme,
projectDir: "",
devPid: null as number | null,
language: "zh" as Language,
},
(set) => ({
// 设置访问令牌
setAccessToken: (token: string) => set({ accessToken: token }),
// 设置主题
setTheme: (theme: Theme) => set({ theme }),
// 设置项目目录
setProjectDir: (path: string) => set({ projectDir: path }),
// 设置开发进程ID
setDevPid: (pid: number | null) => set({ devPid: pid }),
// 设置语言
setLanguage: (lang: Language) => set({ language: lang }),
}),
),
{
name: "app-store",
storage: createJSONStorage(() => store),
// 指定需要持久化的状态字段
partialize: (state) => ({
accessToken: state.accessToken,
theme: state.theme,
projectDir: state.projectDir,
language: state.language,
}),
},
),
shallow,
);
这个实现的关键点包括:
- 创建
TauriStoreState
实例并初始化 - 在选项中配置
onRehydrate
回调函数,用于在数据更新时重新加载 store - 实现自定义的
saveFn
,调用storeSave
命令实现多窗口同步 - 使用
persist
中间件实现状态持久化 - 使用
partialize
配置选择需要持久化的状态字段
3. 实现多窗口同步功能
为了实现多窗口间的数据同步,我们需要创建一个自定义的 Tauri 命令:
rust
// src-tauri/src/commands/utils.rs
#[tauri::command]
pub async fn store_save(app: AppHandle, store_name: &str, current_window: &str) -> Result<()> {
// 保存指定的存储到磁盘
app.get_store(store_name)
.ok_or_else(|| anyhow::anyhow!("Store not found: {}", store_name))?
.save()
.map_err(|e| anyhow!(e))?;
// 向除当前窗口外的所有窗口发送 "rehydrate" 事件
for (label, window) in app.webview_windows().iter() {
if label != current_window {
window.emit("rehydrate", ()).map_err(|e| anyhow!(e))?;
}
}
Ok(())
}
这个命令的工作原理:
- 保存指定的 Store 到磁盘
- 向除当前窗口外的所有窗口发送 "rehydrate" 事件
- 其他窗口监听到 "rehydrate" 事件后会重新加载 Store 数据
4. 注册插件和命令
在 Tauri 应用中注册插件和命令:
rust
// src-tauri/src/lib.rs
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![
crate::commands::utils::store_save,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
工作流程
整个实现的工作流程如下:
- 当 Zustand 状态发生变化时,
setItem
方法会被调用 setItem
方法使用防抖技术优化性能,避免频繁写入- 数据被保存到 Tauri Store 中
- 如果配置了自定义的
saveFn
,则会调用storeSave
命令 storeSave
命令保存 Store 并向其他窗口发送 "rehydrate" 事件- 其他窗口接收到事件后重新加载 Store 数据,实现数据同步
使用示例
在组件中使用这个 Store 非常简单:
typescript
import { useAppStore } from "@/store/useAppStore";
function MyComponent() {
const [theme, setTheme] = useAppStore(
(state) => [state.theme, state.setTheme],
shallow
);
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
);
}
当用户更改主题时,这个更改会自动持久化,并且在所有打开的窗口中同步。
总结
通过结合 Tauri Plugin-Store 和 Zustand,我们可以轻松实现应用状态的持久化和多窗口数据同步。这种方法具有以下优势:
- 简单易用:基于现有库构建,API 简单直观
- 性能优化:使用防抖技术减少不必要的磁盘写入
- 跨窗口同步:通过事件机制实现多窗口数据同步
- 类型安全:完整的 TypeScript 支持
- 灵活配置:支持自定义保存逻辑和防抖延迟
这种方法可以应用于任何需要持久化状态和多窗口同步的 Tauri 应用程序中。