进程间通信IPC
进程间通信 IPC 🔌
由于主进程与渲染进程是相互隔离的,它们之间的所有数据交换都必须通过 IPC(Inter-Process Communication,进程间通信) 完成。掌握 IPC 是 Electron 开发的必备技能。
IPC 通信模式 💎
Electron 提供了三种主要的 IPC 通信模式:
| 模式 | API | 通信方向 | 是否需要返回值 |
|---|---|---|---|
| 单向发送 | ipcRenderer.send + ipcMain.on | 渲染进程 → 主进程 | 否 |
| 双向调用 | ipcRenderer.invoke + ipcMain.handle | 渲染进程 → 主进程 | 是(Promise) |
| 主动推送 | webContents.send + ipcRenderer.on | 主进程 → 渲染进程 | 否 |
模式一:单向发送(send / on) 💎
适用场景:渲染进程通知主进程执行某个操作,不需要返回结果。
渲染进程 主进程
│ │
│ ipcRenderer.send('event', data) │
│ ─────────────────────────────────> │
│ │ ipcMain.on('event', (e, data) => {})
│ │主进程
// src/main/index.ts
import { ipcMain } from 'electron'
// 监听渲染进程消息
ipcMain.on('save-settings', (_, settings) => {
console.log('收到设置:', settings)
// 执行保存操作(写入文件、数据库等)
})预加载脚本
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
saveSettings: (settings: any) => ipcRenderer.send('save-settings', settings),
})渲染进程
// src/renderer/src/main.ts
const saveBtn = document.getElementById('save-btn')
saveBtn?.addEventListener('click', () => {
;(window as any).api.saveSettings({ theme: 'dark', lang: 'zh-CN' })
})模式二:双向调用(invoke / handle) 💎
适用场景:渲染进程调用主进程的能力,需要拿到返回值。这是最常用的模式。
渲染进程 主进程
│ │
│ ipcRenderer.invoke('event', arg) │
│ ─────────────────────────────────> │
│ │ ipcMain.handle('event', async (e, arg) => {...})
│ Promise<T> │
│ <───────────────────────────────── │主进程
// src/main/index.ts
import { ipcMain, dialog } from 'electron'
// handle 返回 Promise,渲染进程可以用 await 接收
ipcMain.handle('pick-file', async (_, options: { multi: boolean }) => {
const result = await dialog.showOpenDialog({
properties: options.multi ? ['openFile', 'multiSelections'] : ['openFile'],
})
return result.filePaths
})
ipcMain.handle('read-file', async (_, filePath: string) => {
const fs = await import('fs/promises')
return fs.readFile(filePath, 'utf-8')
})预加载脚本
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
pickFile: (options: { multi: boolean }) =>
ipcRenderer.invoke('pick-file', options),
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
})渲染进程
// src/renderer/src/main.ts
const pickBtn = document.getElementById('pick-btn')
pickBtn?.addEventListener('click', async () => {
// 直接 await 拿到主进程返回的结果
const files = await (window as any).api.pickFile({ multi: true })
console.log('选中的文件:', files)
if (files.length) {
const content = await (window as any).api.readFile(files[0])
console.log('文件内容:', content)
}
})模式三:主动推送(webContents.send) 💎
适用场景:主进程主动通知渲染进程(不是由渲染进程发起)。例如:菜单点击后通知渲染进程、文件下载进度更新、应用退出前的提示等。
渲染进程 主进程
│ │
│ │ webContents.send('event', data)
│ <───────────────────────────────── │
│ ipcRenderer.on('event', ...) │
│ │主进程
// src/main/index.ts
import { BrowserWindow, Menu } from 'electron'
const createWindow = () => {
const win = new BrowserWindow({ /* ... */ })
// 监听渲染进程加载完成
win.webContents.on('did-finish-load', () => {
// 主动推送一条消息给渲染进程
win.webContents.send('app-ready', {
version: '1.0.0',
platform: process.platform,
})
})
return win
}
// 自定义菜单:点击菜单项后通知渲染进程
const menu = Menu.buildFromTemplate([
{
label: '文件',
submenu: [
{
label: '打开新窗口',
click: () => {
// 通过 webContents.send 通知所有窗口
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('menu-action', 'open-new-window')
})
},
},
],
},
])
Menu.setApplicationMenu(menu)预加载脚本
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
onAppReady: (callback: (info: any) => void) => {
ipcRenderer.on('app-ready', (_, info) => callback(info))
},
onMenuAction: (callback: (action: string) => void) => {
ipcRenderer.on('menu-action', (_, action) => callback(action))
},
})渲染进程
// src/renderer/src/main.ts
// 应用就绪时
;(window as any).api.onAppReady((info: any) => {
console.log('应用就绪:', info)
})
// 菜单操作
;(window as any).api.onMenuAction((action: string) => {
if (action === 'open-new-window') {
console.log('触发打开新窗口')
}
})实战案例:完整的用户登录 💎
下面我们用 IPC 实现一个完整的"用户登录"流程,演示三种模式的综合应用:
场景描述
- 渲染进程发起登录请求(invoke/handle)
- 主进程调用后端 API,验证成功后保存登录状态
- 主进程主动推送"登录成功"事件给渲染进程(webContents.send)
// src/main/index.ts
import { app, BrowserWindow, ipcMain, Menu } from 'electron'
import { join } from 'path'
import axios from 'axios'
let mainWindow: BrowserWindow | null = null
// 模拟从后端验证用户
async function loginRequest(username: string, password: string) {
const response = await axios.post('https://api.example.com/login', {
username,
password,
})
return response.data
}
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
},
})
mainWindow.loadFile('index.html')
// 双向:处理登录请求
ipcMain.handle('user:login', async (_, { username, password }) => {
try {
const userInfo = await loginRequest(username, password)
// 主动推送:通知所有窗口登录成功
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('user:login-success', userInfo)
})
return { success: true, data: userInfo }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 单向:处理登出通知
ipcMain.on('user:logout', () => {
console.log('用户已登出')
})
}// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
// 双向调用
login: (username: string, password: string) =>
ipcRenderer.invoke('user:login', { username, password }),
// 单向发送
logout: () => ipcRenderer.send('user:logout'),
// 监听主动推送
onLoginSuccess: (callback: (user: any) => void) => {
ipcRenderer.on('user:login-success', (_, user) => callback(user))
},
})// src/renderer/src/main.ts
// 登录表单提交
const form = document.getElementById('login-form') as HTMLFormElement
form?.addEventListener('submit', async (e) => {
e.preventDefault()
const username = (document.getElementById('username') as HTMLInputElement).value
const password = (document.getElementById('password') as HTMLInputElement).value
const result = await (window as any).api.login(username, password)
if (result.success) {
console.log('登录成功:', result.data)
} else {
alert('登录失败:' + result.error)
}
})
// 监听主进程推送的登录成功事件
;(window as any).api.onLoginSuccess((user: any) => {
console.log('收到主进程推送:用户', user.name, '已登录')
// 更新 UI:显示头像、用户名等
})IPC 高级技巧 💎
传递复杂数据 👻
IPC 支持传递任何可被 structured clone 序列化的数据:
// 主进程
ipcMain.handle('get-user-list', async () => {
return {
code: 0,
data: [
{ id: 1, name: '张三', tags: ['管理员', 'VIP'] },
{ id: 2, name: '李四', tags: ['普通用户'] },
],
message: 'ok',
}
})不支持的数据类型
- 函数 / Symbol
- DOM 节点
- 含有循环引用的对象
如需传递这些数据,需要先做转换(如函数转字符串、循环引用做去重)。
错误处理 👻
invoke 返回的 Promise 不会自动 reject,即使主进程中抛出异常。需要手动处理:
// 主进程:推荐用 try/catch 包装
ipcMain.handle('do-something', async (_, arg) => {
try {
const result = await someAsyncWork(arg)
return { success: true, data: result }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 渲染进程
const result = await (window as any).api.doSomething(arg)
if (!result.success) {
console.error(result.error)
}移除监听 👻
如果组件销毁时忘了移除 ipcRenderer.on,会导致内存泄漏。需要手动移除:
// 预加载脚本返回 off 方法
contextBridge.exposeInMainWorld('api', {
onLoginSuccess: (callback: (user: any) => void) => {
const handler = (_: any, user: any) => callback(user)
ipcRenderer.on('user:login-success', handler)
// 返回一个移除监听的函数
return () => ipcRenderer.removeListener('user:login-success', handler)
},
})
// 渲染进程(Vue 组件)
onMounted(() => {
offLogin = (window as any).api.onLoginSuccess((user) => {
// ...
})
})
onUnmounted(() => {
offLogin?.()
})MessagePort 与 MessageChannel 👻
对于频繁的双向通信(如流式响应),用 send / invoke 会比较慢,可以使用 MessagePort:
// 主进程
import { MessageChannelMain } from 'electron'
app.whenReady().then(() => {
const { port1, port2 } = new MessageChannelMain()
win.webContents.postMessage('port', null, [port2])
port1.on('message', (e) => {
console.log('主进程收到:', e.data)
port1.postMessage('reply from main')
})
port1.start()
})何时使用 MessagePort?
普通的 invoke 已经能满足大部分需求。但如果遇到以下场景,建议使用 MessagePort:
- 需要流式传输大量数据
- 双向通信频率非常高(如实时日志、文件分片上传)
- 需要长期稳定的双向通道
常见问题 💎
渲染进程无法调用 invoke 返回 undefined 👻
通常是主进程没有注册对应的 handle,或者 channel 名不一致:
// 主进程
ipcMain.handle('my-channel', async (_, args) => {
// ...
})
// 预加载
ipcRenderer.invoke('my-channel', args) // channel 名要完全一致preload 中的 API 在渲染进程访问不到 👻
通常是 contextBridge.exposeInMainWorld 没写对,或者 webPreferences.contextIsolation 没开启:
// 检查主进程 webPreferences
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必须开启
},
})send 出去的消息主进程没收到 👻
检查 event.returnValue 或 event.sender.send 用法。ipcMain.on 不会返回数据给渲染进程。
小结
本章我们学习了 Electron 的三种 IPC 通信模式:
- send / on:单向通知,不需要返回值
- invoke / handle:双向调用,返回 Promise(最常用)
- webContents.send:主进程主动推送
下一章我们将学习 窗口管理,包括创建、配置、多窗口管理等。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。