Electron 进程间通信(IPC)完全指南
前言
IPC(Inter-Process Communication,进程间通信)是 Electron 开发中最重要的概念之一。主进程和渲染进程需要频繁交互,而 IPC 就是它们之间的桥梁。
我记得刚开始学 Electron 时,对 ipcMain.handle、ipcRenderer.invoke、send/on 这些 API 感到困惑。什么时候用哪个?它们有什么区别?经过大量实践,我总结出了一套清晰的使用模式。
今天,我们就来系统地学习 Electron 的 IPC 通信机制。
IPC 通信概览
通信方向
渲染进程 → 主进程
- ipcRenderer.invoke() → ipcMain.handle()
- ipcRenderer.send() → ipcMain.on()
主进程 → 渲染进程
- webContents.send() → ipcRenderer.on()
渲染进程 ↔ 渲染进程
- 通过主进程中转API 对照表
| 渲染进程 API | 主进程 API | 通信方式 | 返回值 |
|---|---|---|---|
invoke() | handle() | 异步双向 | Promise |
send() | on() | 单向 | 无 |
sendSync() | on() | 同步 | 直接返回 |
on() | send() | 被动接收 | 无 |
异步双向通信(推荐)
基础用法
这是最常用、最推荐的通信方式。
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
getUserData: (userId) => ipcRenderer.invoke('get-user-data', userId),
saveUserData: (userData) => ipcRenderer.invoke('save-user-data', userData)
})
// main.js
const { ipcMain } = require('electron')
ipcMain.handle('get-user-data', async (event, userId) => {
console.log('获取用户数据:', userId)
// 模拟异步操作
const userData = await database.findUser(userId)
return userData
})
ipcMain.handle('save-user-data', async (event, userData) => {
console.log('保存用户数据:', userData)
const success = await database.saveUser(userData)
return { success, timestamp: Date.now() }
})
// renderer.js
async function loadUserData() {
try {
const userData = await window.electronAPI.getUserData(123)
console.log('用户数据:', userData)
} catch (error) {
console.error('加载失败:', error)
}
}
async function saveUserData() {
try {
const result = await window.electronAPI.saveUserData({
id: 123,
name: 'John Doe',
email: 'john@example.com'
})
if (result.success) {
console.log('保存成功,时间:', result.timestamp)
}
} catch (error) {
console.error('保存失败:', error)
}
}错误处理
javascript
// main.js
ipcMain.handle('risky-operation', async (event, data) => {
try {
// 验证输入
if (!data || typeof data !== 'object') {
throw new Error('无效的数据格式')
}
// 执行操作
const result = await performOperation(data)
return { success: true, data: result }
} catch (error) {
// 错误会自动传递给渲染进程
throw new Error(`操作失败: ${error.message}`)
}
})
// renderer.js
try {
const result = await window.electronAPI.riskyOperation(data)
console.log('成功:', result)
} catch (error) {
// 捕获主进程抛出的错误
console.error('错误:', error.message)
showErrorNotification(error.message)
}进度回调
对于耗时操作,可以通过事件发送进度:
javascript
// main.js
const { ipcMain } = require('electron')
ipcMain.handle('process-files', async (event, files) => {
const total = files.length
for (let i = 0; i < total; i++) {
await processFile(files[i])
// 发送进度事件
event.sender.send('process-progress', {
current: i + 1,
total,
percentage: Math.round(((i + 1) / total) * 100)
})
}
return { success: true, processed: total }
})
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
processFiles: (files) => ipcRenderer.invoke('process-files', files),
onProgress: (callback) => {
ipcRenderer.on('process-progress', (event, progress) => {
callback(progress)
})
}
})
// renderer.js
// 监听进度
window.electronAPI.onProgress((progress) => {
console.log(`进度: ${progress.percentage}%`)
updateProgressBar(progress.percentage)
})
// 开始处理
const result = await window.electronAPI.processFiles(fileList)
console.log('处理完成:', result)单向消息传递
渲染进程发送消息
适用于不需要返回值的操作:
javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
logMessage: (message) => ipcRenderer.send('log-message', message),
trackEvent: (eventName, data) => ipcRenderer.send('track-event', eventName, data)
})
// main.js
ipcMain.on('log-message', (event, message) => {
console.log('[Renderer]', message)
// 写入日志文件
logger.info(message)
})
ipcMain.on('track-event', (event, eventName, data) => {
console.log('事件跟踪:', eventName, data)
// 发送到分析服务
analytics.track(eventName, data)
})
// renderer.js
window.electronAPI.logMessage('应用启动')
window.electronAPI.trackEvent('button_click', { button: 'save' })主进程推送消息
javascript
// main.js
const { BrowserWindow } = require('electron')
function broadcastMessage(channel, data) {
const windows = BrowserWindow.getAllWindows()
windows.forEach(window => {
window.webContents.send(channel, data)
})
}
// 定时推送
setInterval(() => {
broadcastMessage('server-status', {
status: 'online',
timestamp: Date.now()
})
}, 5000)
// 数据更新时推送
function onDataUpdate(newData) {
broadcastMessage('data-updated', newData)
}
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onServerStatus: (callback) => {
ipcRenderer.on('server-status', (event, status) => {
callback(status)
})
},
onDataUpdated: (callback) => {
ipcRenderer.on('data-updated', (event, data) => {
callback(data)
})
}
})
// renderer.js
window.electronAPI.onServerStatus((status) => {
console.log('服务器状态:', status)
updateStatusIndicator(status)
})
window.electronAPI.onDataUpdated((data) => {
console.log('数据更新:', data)
refreshUI(data)
})同步通信(谨慎使用)
同步调用
⚠️ 注意:同步 IPC 会阻塞渲染进程,应尽量避免使用。
javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
getConfigSync: () => ipcRenderer.sendSync('get-config')
})
// main.js
ipcMain.on('get-config', (event) => {
// 同步返回
event.returnValue = {
theme: 'dark',
language: 'zh-CN'
}
})
// renderer.js
// ⚠️ 这会阻塞渲染进程
const config = window.electronAPI.getConfigSync()
console.log('配置:', config)何时使用同步 IPC
javascript
// ✅ 可以接受的场景:应用启动时获取配置
app.whenReady().then(() => {
const config = loadConfigSync()
createWindow(config)
})
// ❌ 不推荐的场景:频繁调用或耗时操作
button.onclick = () => {
const result = window.electronAPI.heavyOperationSync() // 会卡住界面
}窗口间通信
通过主进程中转
javascript
// main.js
const windows = {
main: null,
settings: null
}
// 创建窗口时保存引用
function createMainWindow() {
windows.main = new BrowserWindow({...})
}
function createSettingsWindow() {
windows.settings = new BrowserWindow({...})
}
// 中转消息
ipcMain.on('message-to-settings', (event, data) => {
if (windows.settings) {
windows.settings.webContents.send('message-from-main', data)
}
})
ipcMain.on('message-to-main', (event, data) => {
if (windows.main) {
windows.main.webContents.send('message-from-settings', data)
}
})
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
sendToSettings: (data) => ipcRenderer.send('message-to-settings', data),
sendToMain: (data) => ipcRenderer.send('message-to-main', data),
onMessageFromMain: (callback) => {
ipcRenderer.on('message-from-main', (event, data) => callback(data))
},
onMessageFromSettings: (callback) => {
ipcRenderer.on('message-from-settings', (event, data) => callback(data))
}
})
// 主窗口 renderer
window.electronAPI.sendToSettings({ action: 'update-theme', theme: 'dark' })
window.electronAPI.onMessageFromSettings((data) => {
console.log('收到设置窗口的消息:', data)
})
// 设置窗口 renderer
window.electronAPI.onMessageFromMain((data) => {
console.log('收到主窗口的消息:', data)
if (data.action === 'update-theme') {
updateTheme(data.theme)
}
})
window.electronAPI.sendToMain({ action: 'settings-saved' })使用 MessagePort
更高级的窗口间通信方式:
javascript
// main.js
const { MessageChannelMain } = require('electron')
ipcMain.handle('create-message-channel', (event) => {
const { port1, port2 } = new MessageChannelMain()
// 将 port1 发送给当前窗口
event.sender.postMessage('port1', null, [port1])
// 将 port2 发送给另一个窗口
settingsWindow.webContents.postMessage('port2', null, [port2])
})
// renderer.js (窗口 A)
const { port1 } = await window.electronAPI.createMessageChannel()
port1.onmessage = (event) => {
console.log('收到消息:', event.data)
}
port1.postMessage({ type: 'greeting', message: 'Hello!' })
// renderer.js (窗口 B)
port2.onmessage = (event) => {
console.log('收到消息:', event.data)
// 直接回复
port2.postMessage({ type: 'reply', message: 'Hi there!' })
}实战模式
1. 请求-响应模式
javascript
// main.js - API 层
class APIHandler {
constructor() {
this.setupHandlers()
}
setupHandlers() {
// 用户相关
ipcMain.handle('api:user:get', this.getUser.bind(this))
ipcMain.handle('api:user:update', this.updateUser.bind(this))
ipcMain.handle('api:user:delete', this.deleteUser.bind(this))
// 文件相关
ipcMain.handle('api:file:read', this.readFile.bind(this))
ipcMain.handle('api:file:write', this.writeFile.bind(this))
}
async getUser(event, userId) {
try {
const user = await database.users.findById(userId)
return { success: true, data: user }
} catch (error) {
return { success: false, error: error.message }
}
}
async updateUser(event, userId, updates) {
try {
await database.users.update(userId, updates)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
// ... 其他方法
}
// preload.js
contextBridge.exposeInMainWorld('api', {
user: {
get: (id) => ipcRenderer.invoke('api:user:get', id),
update: (id, updates) => ipcRenderer.invoke('api:user:update', id, updates),
delete: (id) => ipcRenderer.invoke('api:user:delete', id)
},
file: {
read: (path) => ipcRenderer.invoke('api:file:read', path),
write: (path, content) => ipcRenderer.invoke('api:file:write', path, content)
}
})
// renderer.js
const result = await window.api.user.get(123)
if (result.success) {
console.log('用户数据:', result.data)
}2. 事件总线模式
javascript
// main.js - 事件总线
class EventBus {
constructor() {
this.listeners = new Map()
}
emit(event, data) {
const windows = BrowserWindow.getAllWindows()
windows.forEach(window => {
window.webContents.send('event:' + event, data)
})
}
on(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(handler)
}
}
const eventBus = new EventBus()
// 触发事件
eventBus.emit('user:login', { userId: 123, username: 'john' })
eventBus.emit('data:updated', { table: 'users', id: 123 })
// preload.js
contextBridge.exposeInMainWorld('events', {
on: (event, callback) => {
ipcRenderer.on('event:' + event, (e, data) => callback(data))
},
once: (event, callback) => {
ipcRenderer.once('event:' + event, (e, data) => callback(data))
}
})
// renderer.js
window.events.on('user:login', (data) => {
console.log('用户登录:', data)
updateUI(data)
})
window.events.on('data:updated', (data) => {
console.log('数据更新:', data)
refreshData(data.table, data.id)
})3. 命令模式
javascript
// main.js - 命令处理器
class CommandHandler {
constructor() {
this.commands = new Map()
this.registerCommands()
this.setupIPC()
}
registerCommands() {
this.commands.set('open-file', async (args) => {
const { filePaths } = await dialog.showOpenDialog(args)
return filePaths[0]
})
this.commands.set('save-file', async (args) => {
const { filePath } = await dialog.showSaveDialog(args)
if (filePath) {
await fs.promises.writeFile(filePath, args.content)
}
return filePath
})
this.commands.set('show-notification', (args) => {
new Notification({
title: args.title,
body: args.body
}).show()
})
}
setupIPC() {
ipcMain.handle('execute-command', async (event, commandName, args) => {
const command = this.commands.get(commandName)
if (!command) {
throw new Error(`Unknown command: ${commandName}`)
}
return await command(args)
})
}
}
// preload.js
contextBridge.exposeInMainWorld('commands', {
execute: (name, args) => ipcRenderer.invoke('execute-command', name, args)
})
// renderer.js
async function openFile() {
const filePath = await window.commands.execute('open-file', {
filters: [{ name: 'Text', extensions: ['txt'] }]
})
console.log('选择的文件:', filePath)
}
async function saveFile(content) {
const filePath = await window.commands.execute('save-file', {
content,
defaultPath: 'untitled.txt'
})
console.log('保存到:', filePath)
}
function showNotification(title, body) {
window.commands.execute('show-notification', { title, body })
}性能优化
1. 批量操作
javascript
// ❌ 不好的做法:频繁调用
for (let i = 0; i < 1000; i++) {
await window.electronAPI.saveItem(items[i])
}
// ✅ 好的做法:批量处理
await window.electronAPI.saveItems(items)2. 节流和防抖
javascript
// preload.js
const { debounce, throttle } = require('lodash')
contextBridge.exposeInMainWorld('electronAPI', {
// 防抖:等待用户停止输入
searchDebounced: debounce((query) => {
return ipcRenderer.invoke('search', query)
}, 300),
// 节流:限制调用频率
updatePositionThrottled: throttle((x, y) => {
ipcRenderer.send('update-position', x, y)
}, 100)
})3. 数据压缩
javascript
// 对于大量数据,可以压缩传输
const zlib = require('zlib')
// main.js
ipcMain.handle('get-large-data', async () => {
const data = await loadLargeDataset()
const compressed = zlib.gzipSync(JSON.stringify(data))
return compressed.toString('base64')
})
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
getLargeData: async () => {
const compressed = await ipcRenderer.invoke('get-large-data')
const buffer = Buffer.from(compressed, 'base64')
const decompressed = zlib.gunzipSync(buffer)
return JSON.parse(decompressed.toString())
}
})安全最佳实践
1. 输入验证
javascript
// main.js
ipcMain.handle('save-file', async (event, filePath, content) => {
// 验证路径
if (!isValidPath(filePath)) {
throw new Error('Invalid file path')
}
// 验证内容
if (typeof content !== 'string') {
throw new Error('Content must be a string')
}
// 限制文件大小
if (content.length > 10 * 1024 * 1024) { // 10MB
throw new Error('File too large')
}
await fs.promises.writeFile(filePath, content)
})2. 权限检查
javascript
// main.js
const authorizedWindows = new Set()
function authorizeWindow(window) {
authorizedWindows.add(window.id)
}
ipcMain.handle('sensitive-operation', (event, data) => {
const sender = BrowserWindow.fromWebContents(event.sender)
if (!authorizedWindows.has(sender.id)) {
throw new Error('Unauthorized')
}
return performSensitiveOperation(data)
})3. 速率限制
javascript
// main.js
const rateLimiter = new Map()
ipcMain.handle('api-call', async (event, endpoint, data) => {
const windowId = BrowserWindow.fromWebContents(event.sender).id
const key = `${windowId}:${endpoint}`
// 检查速率限制
const lastCall = rateLimiter.get(key) || 0
const now = Date.now()
if (now - lastCall < 1000) { // 1秒内只能调用一次
throw new Error('Rate limit exceeded')
}
rateLimiter.set(key, now)
return await callAPI(endpoint, data)
})调试技巧
1. IPC 日志
javascript
// main.js
const originalHandle = ipcMain.handle.bind(ipcMain)
ipcMain.handle = function(channel, handler) {
return originalHandle(channel, async (...args) => {
console.log(`[IPC] Handle: ${channel}`, args[1])
const result = await handler(...args)
console.log(`[IPC] Result: ${channel}`, result)
return result
})
}
// renderer.js
const originalInvoke = ipcRenderer.invoke.bind(ipcRenderer)
window.electronAPI.invoke = async function(channel, ...args) {
console.log(`[IPC] Invoke: ${channel}`, args)
const result = await originalInvoke(channel, ...args)
console.log(`[IPC] Response: ${channel}`, result)
return result
}2. 性能监控
javascript
// main.js
ipcMain.handle('monitored-operation', async (event, data) => {
const start = Date.now()
try {
const result = await heavyOperation(data)
const duration = Date.now() - start
console.log(`Operation took ${duration}ms`)
if (duration > 1000) {
console.warn('Slow operation detected!')
}
return result
} catch (error) {
console.error('Operation failed:', error)
throw error
}
})总结
✅ 核心要点
选择合适的通信方式
- 异步双向:
invoke/handle(推荐) - 单向消息:
send/on - 同步调用:谨慎使用
- 异步双向:
安全第一
- 使用 contextBridge
- 验证输入
- 权限控制
性能优化
- 批量操作
- 节流防抖
- 数据压缩
代码组织
- 模块化设计
- 统一的 API 层
- 清晰的命名
🎯 最佳实践
- 优先使用
invoke/handle - 避免同步 IPC
- 做好错误处理
- 监控性能
- 保持代码整洁
下一篇文章,我们将学习窗口管理和菜单系统。
相关文章推荐:
有问题欢迎留言讨论!