问题
如何开发一个electron多窗口状态同步的插件,让Dockview弹出窗之间的数据同步?
设计方案
- 零主进程依赖:基于
BroadcastChannel,不需要修改 Electron 主进程代码,纯前端实现。
- 自动水合(Hydration):新窗口(Dockview 弹出窗)打开时,会自动向老窗口“要”数据,解决初始化空白问题。
- 防死循环:内置互斥锁,防止 A -> B -> A 的无限更新。
- 按需同步:不是所有 Store 都会同步,你可以在定义 Store 时通过
share: true 开启。
1. 核心插件代码
请在项目中新建文件 src/plugins/pinia-plugin-electron-share.ts。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| import { PiniaPluginContext } from 'pinia';
declare module 'pinia' { export interface DefineStoreOptionsBase<S, Store> {
share?: boolean; } }
type SyncPayload = { type: 'UPDATE' | 'REQUEST_INIT' | 'RESPONSE_INIT'; storeId: string; data?: any; timestamp: number; };
const CHANNEL_NAME = 'electron-pinia-share-channel';
export function piniaPluginElectronShare({ store, options }: PiniaPluginContext) { if (!options.share) { return; }
const channel = new BroadcastChannel(CHANNEL_NAME);
let isExternalUpdate = false;
channel.onmessage = (event: MessageEvent<SyncPayload>) => { const { type, storeId, data, timestamp } = event.data;
if (storeId !== store.$id) return;
switch (type) { case 'UPDATE': if (!isExternalUpdate) { isExternalUpdate = true; store.$patch(data); setTimeout(() => { isExternalUpdate = false; }, 0); } break;
case 'REQUEST_INIT': channel.postMessage({ type: 'RESPONSE_INIT', storeId: store.$id, data: JSON.parse(JSON.stringify(store.$state)), timestamp: Date.now(), }); break;
case 'RESPONSE_INIT': isExternalUpdate = true; store.$patch(data); setTimeout(() => { isExternalUpdate = false; }, 0); break; } };
store.$subscribe((mutation, state) => { if (isExternalUpdate) return;
const rawState = JSON.parse(JSON.stringify(state));
channel.postMessage({ type: 'UPDATE', storeId: store.$id, data: rawState, timestamp: Date.now(), }); });
setTimeout(() => { channel.postMessage({ type: 'REQUEST_INIT', storeId: store.$id, timestamp: Date.now(), }); }, 50); }
|
2. 在 Pinia 中注册插件
修改你的 src/store/index.ts (或者你创建 pinia 的地方)。
1 2 3 4 5 6 7 8 9
| import { createPinia } from 'pinia'; import { piniaPluginElectronShare } from '../plugins/pinia-plugin-electron-share';
const pinia = createPinia();
pinia.use(piniaPluginElectronShare);
export default pinia;
|
3. 如何在 Store 中使用
这步最关键。你需要在那些“需要在 Dockview 窗口间同步”的 Store 里,加上 share: true。
例如 src/store/modules/user.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: { name: 'Guest', age: 0 }, layoutConfig: {} }), actions: { updateName(name: string) { this.userInfo.name = name; } },
share: true });
|
如果是 Setup Store 写法:
1 2 3 4 5 6 7 8 9
| export const useCounterStore = defineStore('counter', () => { const count = ref(0); function increment() { count.value++ }
return { count, increment }; }, { share: true });
|
4. 验证与调试方法
我建议你按照以下步骤验证方案是否生效:
- 启动应用:打开主窗口。
- 修改数据:在主窗口修改
useUserStore 中的数据(比如登录,或者改个名字)。
- 弹出窗口 (Dockview):拖拽 Tab 形成一个新的弹出窗口。
- 预期:弹出窗口应该立即显示刚才修改过的数据(这是
REQUEST_INIT 逻辑在起作用)。
- 双向同步测试:
- 在弹出窗口里修改数据。
- 观察主窗口(以及其他弹出窗口)的数据是否跟着变了。
- 预期:应该毫秒级同步。
5. 注意事项 (Senior Tips)
- 同源策略:
BroadcastChannel 依赖浏览器的同源策略。确保你的 electron-vite 在开发模式下所有窗口都是 http://localhost:xxxx,或者在生产模式下加载的都是 file:// 或者自定义协议 app://。只要它们属于同一个 Origin,这个方案就 100% 有效。
- 性能:
虽然 BroadcastChannel 很快,但不要用它传输非常大的二进制数据(如图片 Base64、超长文本)。如果 State 特别大,建议将大数据放在 IndexedDB,Store 里只存 ID。
- 非序列化数据:
JSON.stringify 会丢失 Function、Map、Set 等类型。请确保你的 Pinia State 是纯数据对象(Plain Object)。这是 Vue 和 Pinia 的最佳实践,也是多进程通信的限制。
这套代码虽然看起来简单,但处理了多进程架构中最棘手的“竞争”和“初始化”问题,完全可以直接合入项目。
Prev: 上传到npm仓库
Next: electron 开发者的自我介绍