BroadcastChannel 握手机制的方法实现
使用场景,dokeview tab拖出来窗口外第一次创建窗口的时候传递参数。
将这种“握手”逻辑封装成通用的工具函数,可以大大减少重复代码,并保证逻辑的健壮性。
封装了一个类型安全、支持超时控制的 WindowSync 工具库。
1. 创建工具文件 (src/utils/window-sync.ts)
这个工具包含两个核心函数:
sendToChild: 父窗口调用,等待子窗口就绪后发送数据。
receiveFromParent: 子窗口调用,发送就绪信号并等待数据。
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
|
type SyncMessage<T = any> = | { type: 'CHILD_READY'; id: string } | { type: 'PARENT_DATA'; id: string; payload: T };
interface SyncOptions { timeout?: number; }
export function sendToChild<T>( channelName: string, targetId: string, data: T, options: SyncOptions = {} ): Promise<void> { const { timeout = 5000 } = options;
return new Promise((resolve, reject) => { const channel = new BroadcastChannel(channelName); let timer: any;
const cleanup = () => { channel.close(); clearTimeout(timer); };
timer = setTimeout(() => { cleanup(); reject(new Error(`[WindowSync] Send timeout: Child window "${targetId}" did not respond in ${timeout}ms`)); }, timeout);
channel.onmessage = (event: MessageEvent<SyncMessage>) => { const { type, id } = event.data;
if (type === 'CHILD_READY' && id === targetId) { channel.postMessage({ type: 'PARENT_DATA', id: targetId, payload: data, } as SyncMessage<T>);
cleanup(); resolve(); } }; }); }
export function receiveFromParent<T>( channelName: string, myId: string, options: SyncOptions = {} ): Promise<T> { const { timeout = 5000 } = options;
return new Promise((resolve, reject) => { const channel = new BroadcastChannel(channelName); let timer: any;
const cleanup = () => { channel.close(); clearTimeout(timer); };
timer = setTimeout(() => { cleanup(); reject(new Error(`[WindowSync] Receive timeout: Parent did not send data for "${myId}"`)); }, timeout);
channel.onmessage = (event: MessageEvent<SyncMessage<T>>) => { const { type, id, payload } = event.data;
if (type === 'PARENT_DATA' && id === myId && payload !== undefined) { cleanup(); resolve(payload); } };
setTimeout(() => { channel.postMessage({ type: 'CHILD_READY', id: myId, } as SyncMessage); }, 50); }); }
|
2. 在父窗口中使用 (CustomTab.vue)
现在你的业务代码会变得非常简洁。
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
| <script setup lang="ts"> import { sendToChild } from '../utils/window-sync' // 引入工具
// ...
const performPopout = async () => { const panelId = props.params.api.id const title = props.params.api.title
// 1. 准备大对象数据 const bigData = { panelId, title, userInfo: { name: 'Admin', role: 'SuperUser' }, heavyList: Array.from({ length: 1000 }, (_, i) => i) }
// 2. 打开窗口 (只传基本 ID) const url = `?popout=true&panelId=${panelId}&title=${encodeURIComponent(title)}` window.open(url, '_blank')
// 3. 关闭当前面板 (UI上立即响应) props.params.api.group.api.closePanel(props.params.api.panel)
// 4. 【关键】调用封装好的发送方法 // 不用管什么 addEventListener,一行代码搞定 try { await sendToChild('dockview-popout', panelId, bigData) console.log('✅ 数据发送成功!') } catch (err) { console.error('❌ 发送失败 (可能是窗口被拦截或关闭了):', err) } } </script>
|
3. 在子窗口中使用 (App.vue)
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
| <script setup lang="ts"> import { receiveFromParent } from './utils/window-sync' // 引入工具
const onReady = async (event: DockviewReadyEvent) => { const api = event.api const urlParams = new URLSearchParams(window.location.search) const isPopout = urlParams.get('popout') === 'true' const myPanelId = urlParams.get('panelId')
if (isPopout && myPanelId) { try { // 1. 【关键】调用封装好的接收方法 // 这里支持泛型,会有代码提示 const data = await receiveFromParent<any>('dockview-popout', myPanelId) console.log('👶 收到父窗口投喂:', data)
// 2. 创建面板 api.addPanel({ id: myPanelId, component: 'default', tabComponent: 'myTab', params: { title: data.title, extra: data.userInfo // 使用接收到的数据 }, title: data.title }) } catch (err) { console.error('❌ 获取数据失败:', err) // 降级处理:比如显示一个错误提示或者默认空面板 } } else { // 主窗口逻辑... } } </script>
|
封装后的优势
- 代码解耦:业务组件不需要关心
BroadcastChannel 的创建、销毁、事件监听逻辑。
- 资源自动回收:封装内自动调用了
channel.close(),防止开发者忘记关闭导致内存泄漏。
- 健壮性:增加了
timeout 超时处理。如果新窗口被浏览器拦截了,或者加载失败了,父窗口的 await 不会一直卡死。
- 类型安全:支持 TypeScript 泛型,接收到的 data 直接有类型提示。
Prev: electron多窗口状态同步 插件pinia-plugin-electron-share升级版
Next: 单例 Tab 策略” (Singleton Tab Strategy)