主进程如何主动推送消息给渲染进程?目标窗口如何管理?
考察点
- webContents.send 的使用
- 推送时机(窗口未 ready)、多窗口路由
参考答案
1. 基本方式:webContents.send
// 向指定窗口推送
win.webContents.send('task:progress', { percent: 60 });
// 渲染进程监听(通常在 preload 中封装)
ipcRenderer.on('task:progress', (e, data) => { /* 更新 UI */ });
渲染侧在 preload 里封装成订阅 API:
// preload.js
contextBridge.exposeInMainWorld('api', {
// 订阅主进程推送,返回取消订阅函数
onTaskProgress: (callback) => {
const listener = (e, data) => callback(data);
ipcRenderer.on('task:progress', listener);
return () => ipcRenderer.removeListener('task:progress', listener);
},
});
返回取消订阅函数是重要细节:React/Vue 组件卸载时必须移除监听,否则每次挂载叠加一个监听器,消息重复触发且泄漏内存。
2. 目标窗口管理
主进程不知道"该发给谁"是常见痛点,工程上有三种路由方式:
// 方式一:按业务 key(配合 WindowManager)
windowManager.sendTo('settings', 'config:changed', config);
// 方式二:广播所有窗口(低频消息可用)
windowManager.broadcast('user:logout', null);
// 方式三:按订阅关系(渲染侧先 invoke 订阅,主进程记录 sender)
const subscribers = new Map(); // topic -> Set<webContentsId>
ipcMain.handle('subscribe', (e, topic) => {
if (!subscribers.has(topic)) subscribers.set(topic, new Set());
subscribers.get(topic).add(e.sender.id);
// webContents 销毁时自动清理
e.sender.once('destroyed', () => subscribers.get(topic)?.delete(e.sender.id));
});
方式三适合 agent 场景:如"日志流"只有日志窗口订阅了才推送,避免无脑广播的序列化开销。
3. 推送时机:窗口还没加载完怎么办?
webContents.send 在页面加载完成前调用,消息会丢失(渲染侧还没注册监听)。两个解法:
// 解法一:等 did-finish-load 再推
win.webContents.once('did-finish-load', () => {
win.webContents.send('init:data', data);
});
// 解法二(更通用):渲染侧 ready 后主动 pull
// 渲染进程加载完成后 invoke('app:get-init-data') 拉取初始化数据
实践上初始化数据用 pull(invoke),增量事件用 push(send),职责清晰且没有时序问题。
4. 其他推送通道
- BroadcastChannel:同源窗口间直接广播,不经过主进程,但无法跨 origin、无法定向
- MessagePort:建立通道后点对点推送,适合高频数据
webContents.send到 devtools / 指定 frame:sendToFrame可精确到 iframe
面试话术
推送用 webContents.send,工程上关注三点:订阅关系管理(避免广播风暴)、webContents destroyed 后清理、以及初始化数据用 pull 模式规避页面未 ready 丢消息的时序坑。
常见追问
- webContents 已销毁还 send 会怎样? 抛异常或静默失败,推送前先
isDestroyed()判断。 - 能推给崩溃后重载的窗口吗? 能,webContents 重载后 id 不变,但监听会重新注册。
- 高频推送(如每秒 60 次进度)合适吗? 不合适,结构化克隆 + IPC 开销大,应合并/节流或走 MessagePort。