Tauri(十五)——多窗口之间通信方案

背景

做 Tauri 项目时,你大部分情况会有多个窗口,那么其中就会有多个窗口之间通信的需求。

在前端里边,是单页面应用,共用的一个 Html,而在 Tauri 里边,每个窗口都有自己独立的一个 Html。所以普通的全局参数,默认情况下前端 Store 不跨窗口的。

比如,设置页面设置了一些配置:

  • 修改主题颜色,所有窗口需同步更新。
  • 切换语言时,所有窗口需重新渲染文本。
  • 调整快捷键后,多个窗口需绑定新的事件监听。
  • 两个窗口之间传数据等

方案

1. Tauri 事件系统通信

第一个能想到的就是Tauri 提供了全局事件机制,允许窗口之间发送和监听自定义事件。

发送事件(从窗口 A):

js 复制代码
// 在窗口 A(例如主窗口)
import { emit } from '@tauri-apps/api/event';

// 发送事件到所有窗口(或指定窗口)
await emit('global-message', { data: 'Hello from Window A!' });

监听事件(在窗口 B):

js 复制代码
// 在窗口 B(例如子窗口)
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('global-message', (event) => {
  console.log('Received:', event.payload.data); // 输出: "Hello from Window A!"
});

// 清理监听器(窗口关闭时)
unlisten();
  • 支持定向发送(通过windowLabel指定目标窗口)或全局广播。
  • 适用于简单通知(如状态更新、用户操作触发)。

注意 *:需在tauri.conf.json中开启事件权限(如core:event:allow-emitcore:event:allow-listen

2. Tauri 后端(Rust)转发

窗口之间是独立的,但是 Tauri 的 rust 后端是一份,那么就可以借助 rust 来通信。

利用 Tauri 的后端 Rust 逻辑作为中间人处理复杂通信。

定义 Rust 命令

rs 复制代码
// src/main.rs
#[tauri::command]
fn send_message_to_window(message: String, window_label: &str) {
  app.emit_to(window_label, "backend-message", message).unwrap();
}

前端调用命令

js 复制代码
// 窗口 A 调用 Rust 命令
import { invoke } from '@tauri-apps/api/tauri';

invoke('send_message_to_window', {
  message: 'Data from Window A',
  windowLabel: 'child-window'
});

窗口 B 监听事件

js 复制代码
// 窗口 B
import { listen } from '@tauri-apps/api/event';

listen('backend-message', (event) => {
  console.log('From Rust:', event.payload);
});
  • 安全性:文件读写、加密操作通过Rust处理。
  • 跨进程协调:支持多窗口间复杂逻辑编排(如数据库事务)。

3. Tauri 第三方插件

今天 发现一个社区刚刚发布的第三方插件 tauri-store

  • 将 stores 保存到磁盘。
  • 跨多个窗口同步。
  • 防抖动或限制 stores 更新。
  • 从 JavaScript 和 Rust 访问 stores。

由于我的项目用的是 zustand,那么我们接下来就看看 @tauri-store/zustand

看了文档,使用方式也简单的。那么就好奇,它是怎么实现的,那么我们接下来就看看源码实现。

源码解析

  1. github.com/ferreira-tb...
ts 复制代码
export { 
  createTauriStore, // 创建 Tauri 存储
  tauri, type TauriStore } from './store';


// 导出类型 :
export type {
  MaybePromise,
  Option,
  State,
  StoreBackendOptions,
  StoreFrontendOptions,
  StoreHooks,
  StoreOptions,
  TauriPluginZustandStoreOptions,
  TauriStoreContract,
} from './types';

// 导出命令 :
export {
  clearAutosave,
  getDefaultSaveStrategy,
  getSaveStrategy,
  getStoreCollectionPath,
  getStoreIds,
  getStorePath,
  getStoreState,
  save,
  saveAll,
  saveAllNow,
  saveNow,
  setAutosave,
  setSaveStrategy,
  setStoreCollectionPath,
  setStoreOptions,
} from './commands';
  1. github.com/ferreira-tb...

这段代码定义了一个名为 TauriStore 的类和一个用于创建该类实例的函数 createTauriStore,通过这些实现,可以方便地将状态存储在Tauri应用中,并支持自动保存和同步功能。

ts 复制代码
import * as commands from './commands';
import type { StoreApi } from 'zustand';
import type { TauriPluginZustandStoreOptions } from './types';
import {
  BaseStore,
  debounce,
  DEFAULT_FILTER_KEYS,
  DEFAULT_FILTER_KEYS_STRATEGY,
  DEFAULT_HOOKS,
  DEFAULT_SAVE_ON_CHANGE,
  DEFAULT_SAVE_ON_EXIT,
  type Fn,
  merge,
  type State,
  type StoreHooks,
  type TauriStoreContract,
  throttle,
  TimeStrategy,
} from '@tauri-store/shared';

// 定义一个泛型类 TauriStore,继承自 BaseStore,并实现 TauriStoreContract 接口
export class TauriStore<S extends State, Store extends StoreApi<S>>
  extends BaseStore<S>
  implements TauriStoreContract
{
  public readonly id: string; // 存储的唯一标识符
  public readonly store: Store; // Zustand 的 store 实例
  protected options: TauriPluginZustandStoreOptions<S>; // 存储选项

  // 构造函数,初始化 store 的 id、store 实例和选项
  constructor(id: string, store: Store, options: TauriPluginZustandStoreOptions<S> = {}) {
    super();

    this.id = id;
    this.store = store;

    // 初始化保存和同步策略
    const saveStrategy = new TimeStrategy(options.saveStrategy, options.saveInterval);
    const syncStrategy = new TimeStrategy(options.syncStrategy, options.syncInterval);

    // 设置选项,使用默认值或传入的值
    this.options = {
      filterKeys: options.filterKeys ?? DEFAULT_FILTER_KEYS,
      filterKeysStrategy: options.filterKeysStrategy ?? DEFAULT_FILTER_KEYS_STRATEGY,
      hooks: merge(options.hooks, DEFAULT_HOOKS as StoreHooks<S>),
      saveInterval: saveStrategy.interval,
      saveOnChange: options.saveOnChange ?? DEFAULT_SAVE_ON_CHANGE,
      saveOnExit: options.saveOnExit ?? DEFAULT_SAVE_ON_EXIT,
      saveStrategy: saveStrategy.strategy,
      syncInterval: syncStrategy.interval,
      syncStrategy: syncStrategy.strategy,
    } satisfies Required<TauriPluginZustandStoreOptions<S>>;
  }

  // 加载存储状态
  protected readonly load = async (): Promise<void> => {
    const state = await commands.load<S>(this.id);
    this.patchSelf(state);

    await this.flush();
    this.unwatch = this.watch();
  };

  // 监听 store 的变化
  protected readonly watch = (): Fn => {
    let patchBackend = (state: S) => {
      this.patchBackend(state);
    };

    if (this.syncStrategy === 'debounce') {
      patchBackend = debounce(patchBackend, this.syncInterval);
    } else if (this.syncStrategy === 'throttle') {
      patchBackend = throttle(patchBackend, this.syncInterval);
    }

    return this.store.subscribe((state) => {
      patchBackend(state);
    });
  };

  // 更新自身状态
  protected readonly patchSelf = (state: S): void => {
    let _state = this.options.hooks?.beforeFrontendSync
      ? this.options.hooks.beforeFrontendSync(state)
      : state;

    if (_state) {
      _state = this.applyKeyFilters(_state);
      this.store.setState(_state, /* replace */ false);
    }
  };

  // 更新后端状态
  protected readonly patchBackend = (state: S): void => {
    this.patchBackendHelper(commands.patch, state);
  };

  // 设置选项
  protected readonly setOptions = (): Promise<void> => {
    return this.setOptionsHelper(commands.setStoreOptions);
  };

  // 卸载存储
  protected readonly unload = async (): Promise<void> => {
    await commands.unload(this.id);
  };

  // 获取存储路径
  public readonly getPath = (): Promise<string> => {
    return commands.getStorePath(this.id);
  };

  // 保存当前存储状态
  public readonly save = (): Promise<void> => {
    return commands.save(this.id);
  };

  // 保存所有存储状态
  public readonly saveAll = (): Promise<void> => {
    return commands.saveAll();
  };

  // 立即保存所有存储状态
  public readonly saveAllNow = (): Promise<void> => {
    return commands.saveAllNow();
  };

  // 立即保存当前存储状态
  public readonly saveNow = (): Promise<void> => {
    return commands.saveNow(this.id);
  };
}

/**
 * 创建一个新的 TauriStore 实例
 *
 * @example
 * ```ts
 * import { create } from 'zustand';
 * import { createTauriStore } from '@tauri-store/zustand';
 *
 * type State = {
 *  counter: number;
 * };
 *
 * type Action = {
 *  increase: () => void;
 * };
 *
 * const useCounterStore = create<Action & State>((set) => ({
 *  counter: 0,
 *  increase: () => set((state) => ({ counter: state.counter + 1 })),
 * }))
 *
 * const tauriHandler = createTauriStore('counter-store', useCounterStore);
 * ```
 */
export function createTauriStore<S extends State, Store extends StoreApi<S>>(
  id: string,
  store: Store,
  options: TauriPluginZustandStoreOptions<S> = {}
): TauriStore<S, Store> {
  return new TauriStore(id, store, options);
}

/**
 * `createTauriStore` 的别名
 */
export const tauri = createTauriStore;
  1. github.com/ferreira-tb...

这部分是接下来的核心代码。

ts 复制代码
import { flushPromises } from './utils'; // 导入用于刷新挂起Promise的工具函数
import { listen, StoreEvent } from './event'; // 导入事件监听相关的工具和事件类型
import type { patch, setStoreOptions } from './commands'; // 导入命令类型
import { DEFAULT_FILTER_KEYS_STRATEGY } from './defaults'; // 导入默认的键过滤策略
import { type LooseTimeStrategyKind, TimeStrategy } from './time-strategy'; // 导入时间策略相关类型和类
import type {
  ConfigChangePayload,
  Option,
  State,
  StateChangePayload,
  StoreBackendRawOptions,
  StoreHooks,
  StoreKeyFilter,
  StoreKeyFilterStrategy,
  StoreOptions,
} from './types'; // 导入各种类型定义

/**
 * Base class for the store implementations.
 *
 * @internal
 */
export abstract class BaseStore<S extends State = State> {
  public abstract readonly id: string; // 存储的唯一标识符
  protected abstract readonly options: StoreOptions<S>; // 存储选项

  /** 是否启用同步。 */
  protected enabled = false;
  /** 待处理的状态变化队列。 */
  protected changeQueue: StateChangePayload<S>[] = [];
  /** 刷新挂起的Promise。 */
  protected readonly flush = flushPromises;

  /** 启动存储同步。 */
  public async start(): Promise<void> {
    if (this.enabled) return; // 如果已经启用,则返回
    try {
      this.enabled = true; // 启用同步
      this.unwatch?.(); // 停止当前的状态监听

      await this.load(); // 加载存储状态
      await this.setOptions(); // 设置存储选项

      const unlisten = await this.listen(); // 开始监听状态变化
      this.unlisten?.(); // 停止当前的状态变化监听
      this.unlisten = unlisten; // 保存新的监听函数

      const unlistenOptions = await this.listenOptions(); // 开始监听配置变化
      this.unlistenOptions?.(); // 停止当前的配置变化监听
      this.unlistenOptions = unlistenOptions; // 保存新的监听函数
    } catch (err) {
      this.onError?.(err); // 处理错误
    }
  }

  /** 停止存储同步。 */
  public async stop(): Promise<void> {
    if (!this.enabled) return; // 如果未启用,则返回
    try {
      this.unlistenOptions?.(); // 停止配置变化监听
      this.unlistenOptions = null; // 清除监听函数
      this.unlisten?.(); // 停止状态变化监听
      this.unlisten = null; // 清除监听函数
      this.unwatch?.(); // 停止状态监听
      this.unwatch = null; // 清除监听函数
      this.enabled = false; // 禁用同步
      this.changeQueue = []; // 清空状态变化队列
      await this.unload(); // 卸载存储状态
    } catch (err) {
      this.onError?.(err); // 处理错误
    }
  }

  /** 从后端加载存储状态。 */
  protected abstract load(): Promise<void>;
  protected abstract unload(): Promise<void>;

  /** 监听自身状态变化,必要时通知后端。 */
  protected abstract watch(): () => void;
  /** 停止监听存储状态变化。 */
  protected unwatch: Option<() => void>;

  /** 监听来自后端的状态变化。 */
  protected listen(): Promise<() => void> {
    return listen<StateChangePayload<S>>(StoreEvent.StateChange, ({ payload }) => {
      if (this.enabled && payload.id === this.id) {
        this.changeQueue.push(payload); // 将状态变化加入队列
        this.processChangeQueue().catch((err) => this.onError?.(err)); // 处理状态变化队列
      }
    });
  }

  /** 停止监听来自后端的状态变化。 */
  protected unlisten: Option<() => void>;

  private async listenOptions(): Promise<() => void> {
    return listen<ConfigChangePayload>(StoreEvent.ConfigChange, ({ payload }) => {
      if (this.enabled && payload.id === this.id) {
        this.patchOptions(payload.config); // 更新存储选项
      }
    });
  }

  private unlistenOptions: Option<() => void>;

  protected async processChangeQueue(): Promise<void> {
    while (this.changeQueue.length > 0) {
      await this.flush(); // 刷新挂起的Promise
      const payload = this.changeQueue.pop(); // 取出队列中的状态变化
      if (this.enabled && payload && payload.id === this.id) {
        this.unwatch?.(); // 停止当前的状态监听
        this.unwatch = null; // 清除监听函数
        this.patchSelf(payload.state); // 更新自身状态
        this.changeQueue = []; // 清空状态变化队列
        this.unwatch = this.watch(); // 重新开始状态监听
      }
    }
  }

  protected abstract patchSelf(state: S): void;

  protected abstract patchBackend(state: S): void;

  protected patchBackendHelper(fn: ReturnType<typeof patch>, state: S): void {
    if (this.enabled) {
      let _state = this.options.hooks?.beforeBackendSync
        ? this.options.hooks.beforeBackendSync(state)
        : state; // 应用后端同步前的钩子

      if (_state) {
        _state = this.applyKeyFilters(_state); // 应用键过滤
        fn(this.id, _state).catch((err) => this.onError?.(err)); // 调用后端更新函数
      }
    }
  }

  protected abstract setOptions(): Promise<void>;

  protected async setOptionsHelper(fn: ReturnType<typeof setStoreOptions>): Promise<void> {
    try {
      await fn(this.id, {
        saveInterval: this.options.saveInterval,
        saveOnChange: this.options.saveOnChange,
        saveOnExit: this.options.saveOnExit,
        saveStrategy: this.options.saveStrategy,
      }); // 设置存储选项
    } catch (err) {
      this.onError?.(err); // 处理错误
    }
  }

  private patchOptions(config: StoreBackendRawOptions): void {
    if (typeof config.saveOnChange === 'boolean') {
      this.options.saveOnChange = config.saveOnChange; // 更新保存选项
    }

    if (Array.isArray(config.saveStrategy)) {
      const saveStrategy = TimeStrategy.parse(config.saveStrategy); // 解析保存策略
      this.options.saveInterval = saveStrategy.interval; // 更新保存间隔
      this.options.saveStrategy = saveStrategy.strategy; // 更新保存策略
    }
  }

  protected applyKeyFilters(state: Partial<S>): Partial<S> {
    if (!this.options.filterKeys) {
      return state; // 如果没有过滤键,则返回原状态
    }

    const result: Partial<S> = {};
    const filter = this.options.filterKeys;
    const strategy = this.options.filterKeysStrategy ?? DEFAULT_FILTER_KEYS_STRATEGY;

    for (const [key, value] of Object.entries(state)) {
      if (!shouldFilterKey(filter, strategy, key)) {
        (result as State)[key] = value; // 应用过滤策略
      }
    }

    return result; // 返回过滤后的状态
  }

  /**
   * {@link StoreOptions.syncStrategy}
   */
  protected get syncStrategy(): LooseTimeStrategyKind {
    return this.options.syncStrategy; // 获取同步策略
  }

  /**
   * {@link StoreOptions.saveStrategy}
   */
  protected get syncInterval(): Option<number> {
    return this.options.syncInterval; // 获取同步间隔
  }

  /**
   * {@link StoreHooks.error}
   */
  protected get onError(): Option<StoreHooks<S>['error']> {
    return this.options.hooks?.error; // 获取错误处理钩子
  }
}

function shouldFilterKey(
  filter: StoreKeyFilter,
  strategy: StoreKeyFilterStrategy,
  key: string
): boolean {
  return (
    (strategy === 'omit' && isStoreKeyMatch(filter, key)) ||
    (strategy === 'pick' && !isStoreKeyMatch(filter, key)) ||
    (typeof strategy === 'function' && !strategy(key))
  ); // 判断是否应该过滤键
}

function isStoreKeyMatch(filter: StoreKeyFilter, key: string): boolean {
  return (
    (typeof filter === 'string' && key === filter) ||
    (Array.isArray(filter) && filter.includes(key)) ||
    (filter instanceof RegExp && filter.test(key))
  ); // 判断键是否匹配过滤条件
}
  • 同步机制 : BaseStore 类提供了启动和停止同步的方法,通过监听状态变化和配置变化来实现同步。状态变化通过 changeQueue 队列处理,确保所有变化都被正确处理。
  • 状态过滤 : 使用 applyKeyFilters 方法对状态进行过滤,确保只有符合条件的键值对被同步。
  • 错误处理 : 通过 onError 钩子处理同步过程中的错误。
  • 抽象方法 : BaseStore 定义了一些抽象方法,如 load 、 unload 、 patchSelf 等,具体实现由子类提供。

也是用的 tauri 的 事件系统通信吗?emit 和 listen 吗? 看着像,那就继续看下去...

  1. github.com/ferreira-tb...

通过这段代码可以看出,它使用了 Tauri 事件系统来实现事件监听。通过 webview 窗口来监听和处理特定的存储事件。

ts 复制代码
import type { EventCallback, UnlistenFn } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

export enum StoreEvent {
  ConfigChange = 'tauri-store://config-change',
  StateChange = 'tauri-store://state-change',
  Unload = 'tauri-store://unload',
}

/**
 * Listen for store events emitted to the current webview window.
 */
export function listen<T>(event: StoreEvent, listener: EventCallback<T>): Promise<UnlistenFn> {
  return getCurrentWebviewWindow().listen<T>(event, listener);
}

这么读完源代码之后,我个人暂时不太想用这个库了,就是感觉没有必要,目前我采用的方案就是 zustand 里边通过 **tauri 的 事件系统通信 emit 和 listen **。

源码心得

虽然最后决定不采用该第三方库,但是读了源码,作者的编码能力还是要给出一个大大的赞👍。

该库的代码规范和风格都表现出较高的质量,适合于开发和维护复杂的应用程序。值得我好好学习。最近由于一直在赶项目进度,反复来回的调整修改,对比该库的代码,自己的代码看着有点不尽人意,明天就去修改!JUST DO IT !

小结

  1. 简单通信 :优先使用 Tauri 事件系统
  2. 复杂状态同步 :使用 全局状态管理(如 Zustand + Tauri 持久化 + Tauri 事件系统)。
  3. 安全敏感操作 :通过 Rust 后端 中转。

如有其它更好的方案,希望评论区讨论。

相关推荐
前端极客探险家1 分钟前
React Query 4 核心技术解析:从自动缓存到无限滚动优化
前端·react.js·缓存
Cutey9161 分钟前
解决 Input Number 输入框出现科学计数法(如 -1e-18)的问题
前端·javascript·面试
CodeSam2 分钟前
气泡弹弹弹
前端
前端开发君3 分钟前
前端工程中的文件
前端
我是小七呦4 分钟前
面试官:请你介绍一下BFC
前端·css
知否技术24 分钟前
JSON.parse不是万能药!手把手教你写一个深拷贝!
前端·javascript
henujolly37 分钟前
css100个问题
前端
杨超越luckly42 分钟前
HTML应用指南:利用POST请求获取全国小鹏汽车的充电桩位置信息
前端·信息可视化·数据挖掘·数据分析·html
Anlici1 小时前
2025最新面筋 | 408篇
前端·面试
Triumphlight1 小时前
【useMergeState】react开源组件常用——组件受控与非受控
前端·react.js