使用 Tauri Plugin-Store 实现 Zustand 持久化与多窗口数据同步

使用 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 应用深度集成

将这两者结合使用,我们可以实现:

  1. 应用状态的自动持久化
  2. 多窗口间的数据同步
  3. 高性能的状态管理

核心实现思路

我们的实现方案基于以下核心组件:

  1. Tauri Plugin-Store:提供底层的持久化存储功能
  2. Zustand:提供应用状态管理
  3. 自定义的 TauriStoreState 类:连接 Tauri Store 和 Zustand
  4. 自定义的 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 接口,包含 getItemsetItemremoveItem 方法
  • 使用 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,
);

这个实现的关键点包括:

  1. 创建 TauriStoreState 实例并初始化
  2. 在选项中配置 onRehydrate 回调函数,用于在数据更新时重新加载 store
  3. 实现自定义的 saveFn,调用 storeSave 命令实现多窗口同步
  4. 使用 persist 中间件实现状态持久化
  5. 使用 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(())
}

这个命令的工作原理:

  1. 保存指定的 Store 到磁盘
  2. 向除当前窗口外的所有窗口发送 "rehydrate" 事件
  3. 其他窗口监听到 "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");
}

工作流程

整个实现的工作流程如下:

  1. 当 Zustand 状态发生变化时,setItem 方法会被调用
  2. setItem 方法使用防抖技术优化性能,避免频繁写入
  3. 数据被保存到 Tauri Store 中
  4. 如果配置了自定义的 saveFn,则会调用 storeSave 命令
  5. storeSave 命令保存 Store 并向其他窗口发送 "rehydrate" 事件
  6. 其他窗口接收到事件后重新加载 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,我们可以轻松实现应用状态的持久化和多窗口数据同步。这种方法具有以下优势:

  1. 简单易用:基于现有库构建,API 简单直观
  2. 性能优化:使用防抖技术减少不必要的磁盘写入
  3. 跨窗口同步:通过事件机制实现多窗口数据同步
  4. 类型安全:完整的 TypeScript 支持
  5. 灵活配置:支持自定义保存逻辑和防抖延迟

这种方法可以应用于任何需要持久化状态和多窗口同步的 Tauri 应用程序中。

相关推荐
沙白猿3 小时前
npm启动项目报错“无法加载文件……”
前端·npm·node.js
tyro曹仓舒3 小时前
彻底讲透as const + keyof typeof
前端·typescript
蛋黄液3 小时前
【黑马程序员】后端Web基础--Maven基础和基础知识
前端·log4j·maven
睡不着的可乐3 小时前
uniapp 支付宝小程序 扩展组件 component 节点的class不生效
前端·微信小程序·支付宝
前端小书生3 小时前
React Router
前端·react.js
_大学牲3 小时前
Flutter Liquid Glass 🪟魔法指南:让你的界面闪耀光彩
前端·开源
Miss Stone4 小时前
css练习
前端·javascript·css
Nicholas684 小时前
flutter视频播放器video_player_avfoundation之FVPVideoPlayer(二)
前端
文心快码BaiduComate4 小时前
一人即团队,SubAgent引爆开发者新范式
前端·后端·程序员