IPC(进程间通信)是 Electron 应用中最常打交道的机制。在第九章铺垫了主进程与渲染进程的职责后,这一节我们聚焦到最基础、也最常用的通信方式:基于 send 和 on 的单向事件通知。它的特点是“发出去就不管了”——发送方把消息抛出,接收方监听并处理,不需要立即返回结果。这种松散耦合的模式非常适合状态同步、菜单动作触发、窗口控制等场景。
9.3.1 什么是单向通信
单向通信指的是消息从发送方单向流向接收方,没有内建的回调或返回值。在 Electron 中,主进程和渲染进程都可以成为发送方或接收方,使用的核心 API 是:
ipcMain.on(channel, listener)—— 主进程监听某个频道ipcRenderer.send(channel, ...args)—— 渲染进程向主进程发送消息webContents.send(channel, ...args)—— 主进程向指定渲染进程发送消息ipcRenderer.on(channel, listener)—— 渲染进程监听来自主进程的消息
这一套 API 完全基于 Node.js 的 EventEmitter 模式,因此你可以在同一个频道上绑定多个监听器,也可以用 removeListener 取消监听。消息体可以是任意能被结构化克隆算法序列化的数据:字符串、数字、普通对象、数组等。
9.3.2 渲染进程 → 主进程:通知动作
最常见的场景是用户点击界面上的按钮,需要触发一个由主进程执行的操作,比如关闭窗口、最小化应用、打开文件对话框等。
渲染进程(preload 暴露的接口)
为了安全,我们不会直接在渲染进程中使用 ipcRenderer,而是通过预加载脚本暴露一个安全的方法。
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
closeWindow: () => ipcRenderer.send('window-close'),
minimizeWindow: () => ipcRenderer.send('window-minimize'),
});
主进程监听
// main.js
const { ipcMain, BrowserWindow } = require('electron');
ipcMain.on('window-close', (event) => {
// event.sender 是发送消息的 webContents,
// 可以通过它找到所属的 BrowserWindow
const win = BrowserWindow.fromWebContents(event.sender);
if (win) win.close();
});
ipcMain.on('window-minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) win.minimize();
});
渲染进程调用
// renderer.js
document.getElementById('close-btn').addEventListener('click', () => {
window.electronAPI.closeWindow();
});
这里 ipcRenderer.send 是纯粹的“发射后不管”,主进程收到后执行对应操作,渲染进程不需要等待任何结果。这种模式非常适合那些不需要反馈状态的 trigger 型动作。
9.3.3 主进程 → 渲染进程:推送状态
反向的推送同样高频:菜单栏的某个操作被触发、系统进入休眠/唤醒、托盘图标被点击等,主进程需要把这些事件告知界面层,让渲染进程更新 UI。
主进程推送消息
// main.js
const { Menu, BrowserWindow } = require('electron');
const menuTemplate = [
{
label: '视图',
submenu: [
{
label: '切换侧边栏',
accelerator: 'CmdOrCtrl+B',
click: (menuItem, focusedWindow) => {
// 向焦点窗口的渲染进程发送消息
if (focusedWindow) {
focusedWindow.webContents.send('toggle-sidebar');
}
}
}
]
}
];
渲染进程监听
// preload.js 暴露监听能力
contextBridge.exposeInMainWorld('electronAPI', {
onToggleSidebar: (callback) => ipcRenderer.on('toggle-sidebar', callback),
// 清理监听器的方法也应当暴露
removeToggleSidebarListener: () => ipcRenderer.removeAllListeners('toggle-sidebar')
});
// renderer.js
window.electronAPI.onToggleSidebar(() => {
document.querySelector('.sidebar').classList.toggle('hidden');
});
// 组件销毁时别忘了移除监听,避免内存泄漏
window.addEventListener('beforeunload', () => {
window.electronAPI.removeToggleSidebarListener();
});
这里有一个关键点:ipcRenderer.on 每次调用都会绑定一个新的监听器,如果不主动移除,在页面刷新或组件重载时容易造成重复绑定甚至内存泄漏。因此最佳实践是在不需要监听时调用 ipcRenderer.removeAllListeners 或按引用移除特定监听器。
9.3.4 主进程广播到所有窗口
有些消息需要推送给所有已打开的窗口,比如主题模式切换(深色/浅色)、用户登录状态变更。此时可以通过 BrowserWindow.getAllWindows() 遍历所有窗口并发送:
const { BrowserWindow } = require('electron');
function broadcast(channel, data) {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send(channel, data);
});
}
// 使用
ipcMain.on('theme-change', (event, newTheme) => {
broadcast('theme-changed', newTheme);
});
广播时需注意:如果某个窗口已经被销毁,其 webContents 可能处于已销毁状态,这时发送消息也不会抛出错误(Electron 内部会忽略对已销毁 webContents 的 send 调用),但为严谨起见你仍然可以在发送前检查 win.isDestroyed()。
9.3.5 安全注意事项
从 Electron 12 开始,默认启用了 contextIsolation: true,这意味着渲染进程无法直接访问 Node.js 或 Electron 的 API。任何 IPC 通信都必须通过 preload 脚本用 contextBridge 暴露有限的、安全的函数。这种模式大大降低了 XSS 攻击面:即使攻击者在渲染进程中注入了恶意脚本,他也无法直接调用 ipcRenderer.send,只能调用你暴露的特定方法。
因此,在编写单向通信代码时,请遵循以下原则:
- 永远不要在生产中开启
nodeIntegration: true。 - 不要通过
contextBridge暴露完整的ipcRenderer对象,而是暴露具名的方法(如closeWindow、onToggleSidebar)。 - 验证主进程收到的所有频道和参数,不要信任渲染进程传来的数据,尤其是文件路径、命令这类敏感信息。
9.3.6 实际项目中的典型用法总结
| 通信方向 | API 组合 | 典型场景 |
|---------|---------|---------|
| 渲染进程 → 主进程 | ipcRenderer.send + ipcMain.on | 触发窗口操作、调用系统对话框、保存文件 |
| 主进程 → 渲染进程 | webContents.send + ipcRenderer.on | 菜单事件通知、全局快捷键响应、系统状态推送 |
| 广播 | BrowserWindow.getAllWindows + webContents.send | 主题切换、登录态变更、语言切换 |
send / on 模式足够覆盖绝大多数“通知类”通信需求。它的心智负担低,代码容易调试,是每个 Electron 开发者必须熟练掌握的基本功。当你需要请求-响应式的通信(比如读取文件内容并返回结果)时,下一节的 invoke / handle 模式会更合适。但即使出现了那种需求,单向通信仍然会在你的代码库中占据相当大的比重,因为桌面应用中的大量交互本质上都是“告诉我,我来做”或“我做完了,告诉你一声”——这正是 send / on 最擅长的领域。