Skip to content

Electron 进程间通信(IPC)完全指南

前言

IPC(Inter-Process Communication,进程间通信)是 Electron 开发中最重要的概念之一。主进程和渲染进程需要频繁交互,而 IPC 就是它们之间的桥梁。

我记得刚开始学 Electron 时,对 ipcMain.handleipcRenderer.invokesend/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
  }
})

总结

✅ 核心要点

  1. 选择合适的通信方式

    • 异步双向:invoke/handle(推荐)
    • 单向消息:send/on
    • 同步调用:谨慎使用
  2. 安全第一

    • 使用 contextBridge
    • 验证输入
    • 权限控制
  3. 性能优化

    • 批量操作
    • 节流防抖
    • 数据压缩
  4. 代码组织

    • 模块化设计
    • 统一的 API 层
    • 清晰的命名

🎯 最佳实践

  • 优先使用 invoke/handle
  • 避免同步 IPC
  • 做好错误处理
  • 监控性能
  • 保持代码整洁

下一篇文章,我们将学习窗口管理和菜单系统。


相关文章推荐:

有问题欢迎留言讨论!