问题
我现在做的是多窗口应用(基于electron-vite,vue3,和dokeview),现在有一个场景:我们原项目里面多出地方使用的事件总线模式,在组件挂载的时候注册事件,在组件销毁的时候off事件,现在改成多窗口应用后,就出现了打开了多个注册事件的窗口,事件触发的时候会触发多次,事件关闭的时候就全都关闭了。这种情况 有什么好的解决方案吗?
解决方案
这是一个非常经典的架构演进问题。当你从“单页面应用 (SPA)”转变为“多窗口应用 (Multi-Window)”时,传统的事件总线(EventBus)模式如果不加改造,必然会遇到**“广播风暴”(触发多次)和“误杀”**(关一个全关了)的问题。
问题根源分析
触发多次 (Broadcast Storm):
- 如果你的事件总线是基于 IPC(
ipcRenderer)或 BroadcastChannel 的,消息是广播的。
- 你有 3 个窗口,每个窗口里的组件都监听了
refresh-data。
- 当一个事件发出时,3 个窗口都收到了,导致代码执行了 3 次。
- 但在旧的单页应用思维里,通常预期是谁发出的,或者谁在当前视野里,谁才响应。
全都不触发了 (Premature Unsubscription):
- 这是最常见的问题。你的代码可能写的是
bus.off('event-name')。
- 这会移除该事件的所有监听器。
- 在多窗口(或同一窗口多 Tab)场景下,窗口 A 销毁组件时调用了
off,结果把窗口 B(甚至同窗口下的 Tab B)正在用的监听器也给删掉了。
解决方案
你需要将事件总线升级为 “多级分发策略”。我们需要明确区分:这是给谁的消息?
我为你提供一套 “带作用域的分布式事件总线” 解决方案。
核心策略
- 精确销毁:绝对禁止使用
bus.off('name')(清空所有),必须使用 bus.off('name', handler)(只移除当前函数的引用)。
- 作用域区分 (Scoping):
- Local (本地):只在当前窗口/当前组件内生效(使用
mitt 或简单的回调)。
- Global (全局):所有窗口都生效(例如:用户退出登录)。
- Targeted (定向):只发给特定的
panelId 或 windowId(例如:刷新表格,只刷新 ID 为 user-table 的那个)。
代码实现:DistributedEventBus
我们将封装一个智能的 EventBus,它底层结合了 mitt (本地) 和 BroadcastChannel (跨窗口)。
1. 工具类封装 (src/utils/event-bus.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
| import mitt, { Handler } from 'mitt'
type BusPayload = { event: string data: any targetId?: string sourceId?: string }
export class DistributedEventBus { private localBus = mitt() private channel: BroadcastChannel private myId: string
constructor(channelName: string = 'app-global-bus') { this.myId = Math.random().toString(36).substr(2, 9) this.channel = new BroadcastChannel(channelName)
this.channel.onmessage = (msg: MessageEvent<BusPayload>) => { const { event, data, targetId, sourceId } = msg.data
if (targetId && targetId !== this.myId) return
this.localBus.emit(event, { ...data, _fromRemote: true }) } }
on(event: string, handler: Handler) { this.localBus.on(event, handler) }
off(event: string, handler: Handler) { this.localBus.off(event, handler) }
emit(event: string, data: any = {}, scope: 'local' | 'global' | string = 'local') { this.localBus.emit(event, data)
if (scope === 'local') return
const payload: BusPayload = { event, data, sourceId: this.myId, targetId: scope === 'global' ? undefined : scope }
this.channel.postMessage(payload) } setId(id: string) { this.myId = id }
destroy() { this.localBus.all.clear() this.channel.close() } }
export const bus = new DistributedEventBus()
|
2. 在 Vue 组件中如何正确使用 (解决“全关闭”问题)
关键在于:在 onUnmounted 时,只移除自己的那个回调函数。
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
| <script setup lang="ts"> import { onMounted, onUnmounted } from 'vue' import { bus } from './utils/event-bus'
// 假设这是 Dockview 里的一个 Panel const props = defineProps<{ params: any }>() const myPanelId = props.params.api.id
// 1. 设置 ID,以便能接收定向消息 bus.setId(myPanelId)
// 2. 定义处理函数 (必须提取出来,不能写匿名函数!) // ❌ 错误:bus.on('refresh', () => { ... }) <- 匿名函数没法精确移除 // ✅ 正确: const handleRefresh = (data: any) => { console.log(`[${myPanelId}] 收到刷新请求:`, data) // 业务逻辑... }
onMounted(() => { // 注册 bus.on('refresh-grid', handleRefresh) })
onUnmounted(() => { // 3. 销毁时,只移除当前的 handleRefresh // 这样绝对不会影响其他窗口或 Tab 的 'refresh-grid' 监听 bus.off('refresh-grid', handleRefresh) })
// --- 触发示例 ---
const triggerLocal = () => { // 只刷新我自己这个窗口/Tab bus.emit('refresh-grid', { page: 1 }, 'local') }
const triggerGlobal = () => { // 刷新所有打开这个页面的窗口 (比如用户修改了全局配置) bus.emit('refresh-grid', { theme: 'dark' }, 'global') }
const triggerTarget = () => { // 假设你想刷新 ID 为 'user-list-panel' 的那个 Tab (无论它在哪个窗口) bus.emit('refresh-grid', { reload: true }, 'user-list-panel') } </script>
|
方案优缺点总结
解决“触发多次”:
- 默认
scope: 'local':大多数业务场景(比如点击按钮刷新当前表格)只需要本地触发。旧代码如果不改,默认就是 Local,行为和单页应用一致,不会广播风暴。
- 需要同步时显式用
'global'。
解决“全部关闭”:
- 强制要求传入
handler 进行 off。这是编程习惯的矫正。
mitt 库本身就支持精确移除。
适配 Dockview:
- 通过
setId(panelId),实现了基于 Panel ID 的定向通信。
- 你可以实现:“在窗口 A 点击保存,自动刷新窗口 B 里的列表”这种高级功能。
迁移建议
你现在的项目里可能充斥着 bus.emit 和 bus.off。
- 搜索替换:把引入的老 EventBus 换成这个新的
DistributedEventBus。
- 默认行为:由于默认是
'local',你直接替换后,大部分功能会恢复正常(不再乱广播)。
- 修复销毁逻辑:你需要人工检查所有
onUnmounted 或 beforeDestroy,把 bus.off('event') 改成 bus.off('event', myHandler)。
- 如果老代码全是匿名函数怎么办?
- 临时补救:在
DistributedEventBus 里维护一个 Map<PanelId, List<Handler>>,在组件销毁时调用 bus.clearCurrentScope()。但这比较复杂,推荐还是重构为命名函数。
Prev: electron-vite IDE左侧读取工程文件
Next: electron多窗口状态同步 插件pinia-plugin-electron-share升级版