背景
做 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-emit
和core: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
看了文档,使用方式也简单的。那么就好奇,它是怎么实现的,那么我们接下来就看看源码实现。
源码解析
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';
这段代码定义了一个名为 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;
这部分是接下来的核心代码。
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 吗? 看着像,那就继续看下去...
通过这段代码可以看出,它使用了 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 !
小结
- 简单通信 :优先使用 Tauri 事件系统。
- 复杂状态同步 :使用 全局状态管理(如 Zustand + Tauri 持久化 + Tauri 事件系统)。
- 安全敏感操作 :通过 Rust 后端 中转。
如有其它更好的方案,希望评论区讨论。