人人都会AI编程

9.3 单向通信:send /on 事件通知模式

更新时间:2026-07-11

IPC(进程间通信)是 Electron 应用中最常打交道的机制。在第九章铺垫了主进程与渲染进程的职责后,这一节我们聚焦到最基础、也最常用的通信方式:基于 sendon 的单向事件通知。它的特点是“发出去就不管了”——发送方把消息抛出,接收方监听并处理,不需要立即返回结果。这种松散耦合的模式非常适合状态同步、菜单动作触发、窗口控制等场景。

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 对象,而是暴露具名的方法(如 closeWindowonToggleSidebar)。
  • 验证主进程收到的所有频道和参数,不要信任渲染进程传来的数据,尤其是文件路径、命令这类敏感信息。

9.3.6 实际项目中的典型用法总结

| 通信方向 | API 组合 | 典型场景 |
|---------|---------|---------|
| 渲染进程 → 主进程 | ipcRenderer.send + ipcMain.on | 触发窗口操作、调用系统对话框、保存文件 |
| 主进程 → 渲染进程 | webContents.send + ipcRenderer.on | 菜单事件通知、全局快捷键响应、系统状态推送 |
| 广播 | BrowserWindow.getAllWindows + webContents.send | 主题切换、登录态变更、语言切换 |

send / on 模式足够覆盖绝大多数“通知类”通信需求。它的心智负担低,代码容易调试,是每个 Electron 开发者必须熟练掌握的基本功。当你需要请求-响应式的通信(比如读取文件内容并返回结果)时,下一节的 invoke / handle 模式会更合适。但即使出现了那种需求,单向通信仍然会在你的代码库中占据相当大的比重,因为桌面应用中的大量交互本质上都是“告诉我,我来做”或“我做完了,告诉你一声”——这正是 send / on 最擅长的领域。