03 · IPC 进程间通信:四种模式实例
目标:掌握主进程 ↔ 渲染进程通信的全部模式。IPC 是 Electron 开发的核心——调用原生 API、菜单触发页面更新都靠它。 官方对应:进程间通信
IPC 通道基础
ipcMain(主进程)+ipcRenderer(渲染进程,需经 preload 暴露)- 通道名任意、双向、字符串命名(官方喜欢
dialog:openFile这种命名空间风格,前缀无魔法含义) - 渲染进程不能直接
require('electron')用ipcRenderer,必须 preload +contextBridge - 消息序列化用结构化克隆算法:DOM 对象、C++ 支撑对象(
WebContents、BrowserWindow、process.env)不可传输
模式 1:渲染器 → 主进程(单向)
场景实例:输入框修改窗口标题(ipcRenderer.send + ipcMain.on)
main.js:
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents) // 找到发送方所在窗口
win.setTitle(title)
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') }
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle) // 先注册监听,再建窗口
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})preload.js:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})index.html + renderer.js:
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
window.electronAPI.setTitle(titleInput.value)
})模式 2:渲染器 → 主进程(双向,推荐)
场景实例:打开原生文件对话框并返回路径(ipcRenderer.invoke + ipcMain.handle,Promise 风格)
main.js:
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('node:path')
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0] // 返回值会作为 Promise 结果回到 invoke 调用处
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') }
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})preload.js:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'" />
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src="./renderer.js"></script>
</body>
</html>renderer.js:
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})注意:
handle里抛出的错误会被序列化,渲染器只能拿到message属性(见 electron#24427)。
两种旧方法(了解即可,不推荐)
| 方法 | 缺点 |
|---|---|
ipcRenderer.send + event.reply | 需要另写一个 ipcRenderer.on 收回复;请求与响应难配对 |
ipcRenderer.sendSync + event.returnValue | 同步阻塞渲染进程,避免使用 |
模式 3:主进程 → 渲染器
场景实例:原生菜单控制页面计数器(webContents.send + ipcRenderer.on)
main.js(构建菜单,点击时向渲染器发消息):
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') }
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{ label: 'Increment', click: () => mainWindow.webContents.send('update-counter', 1) },
{ label: 'Decrement', click: () => mainWindow.webContents.send('update-counter', -1) }
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
// 可选:接收渲染器回传的最新值
ipcMain.on('counter-value', (_event, value) => console.log(value))
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})preload.js(注意回调包装方式——安全要点):
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 不要把 callback 直接传给 ipcRenderer.on!
// event.sender 会泄露整个 ipcRenderer 实例
onUpdateCounter: (callback) =>
ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'" />
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>renderer.js:
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue) // 回传给主进程
})主进程→渲染器没有
invoke等价物;需要回复就用ipcRenderer.send另开一条通道回传。
模式 4:渲染器 → 渲染器
没有直连方式,两种选择:
- 主进程中转:A 渲染器 → 主进程 → B 渲染器(简单)
- MessagePort:主进程把端口分发给两个渲染器,建立后直连(高频通信时更高效,见消息端口)
安全红线(必须背下来)
// 错误:暴露整个模块 = 渲染器可发任意通道消息
contextBridge.exposeInMainWorld('api', ipcRenderer)
// 正确:逐个包装具体通道
contextBridge.exposeInMainWorld('api', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})- 永远不要直接暴露
ipcRenderer或其.on/.send/.invoke方法 - 接收类回调要用包装函数只传数据参数,不传
event - 主进程处理特权消息时验证
event.senderFrame的 URL(见 06-security-performance)
模式选择速查
| 需求 | 模式 |
|---|---|
| 页面通知主进程做事,不需要结果 | 模式 1 send/on |
| 页面调用主进程并等返回值(文件操作、查询) | 模式 2 invoke/handle ⭐ 默认选择 |
| 菜单/托盘/快捷键驱动页面更新、推送通知 | 模式 3 webContents.send |
| 多窗口协作 | 模式 4 中转或 MessagePort |
练习
- 综合练习:做一个”记事本”——菜单”打开”触发文件对话框(模式 2),读文件内容显示到页面;菜单”保存”把页面内容写回(模式 2)
- 用模式 3 做一个菜单项”清空计数器”,点击后页面归零
- 开两个窗口,用模式 4(主进程中转)让窗口 A 的输入实时同步到窗口 B
小结
invoke/handle是现代 Electron IPC 的默认姿势(Promise、可配对)- 所有暴露都经
contextBridge白名单包装,绝不裸暴露 - 下一章:04-native-features — 菜单/托盘/通知/快捷键实战