跳到主要内容

进程间通信(IPC)

进程间通信(Inter-Process Communication,IPC)是 Electron 应用的核心机制。由于主进程和渲染进程运行在独立的进程中,它们无法直接共享数据,必须通过 IPC 进行通信。本章将详细介绍 IPC 的工作原理和最佳实践。

IPC 基本概念

为什么需要 IPC

Electron 采用多进程架构,主进程和渲染进程相互隔离:

  • 主进程:拥有完整的 Node.js 能力,可以访问文件系统、调用原生 API
  • 渲染进程:运行 Web 环境,默认无法访问 Node.js

这种隔离设计提高了安全性,但也带来了进程间数据交换的需求。IPC 就是连接这两个进程的桥梁。

IPC 通信方向

IPC 支持三种通信方向:

  1. 渲染进程 → 主进程(单向):渲染进程发送消息,不等待响应
  2. 渲染进程 → 主进程(双向):渲染进程发送消息,等待主进程返回结果
  3. 主进程 → 渲染进程:主进程主动向渲染进程发送消息

ipcRenderer.send - 单向通信

单向通信适用于不需要返回结果的场景,如日志记录、状态更新等。

基本用法

预加载脚本(preload.js)

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (message) => ipcRenderer.send('send-message', message)
})

主进程(main.js)

const { ipcMain } = require('electron')

ipcMain.on('send-message', (event, message) => {
console.log('收到消息:', message)
})

渲染进程(renderer.js)

document.getElementById('sendBtn').addEventListener('click', () => {
window.electronAPI.sendMessage('Hello from renderer!')
})

完整示例:标题更新

预加载脚本

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})

主进程

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')

let mainWindow

function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

app.whenReady().then(createWindow)

ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
if (win) {
win.setTitle(title)
}
})

渲染进程

const titleInput = document.getElementById('titleInput')
const setTitleBtn = document.getElementById('setTitleBtn')

setTitleBtn.addEventListener('click', () => {
const title = titleInput.value
if (title) {
window.electronAPI.setTitle(title)
}
})

ipcRenderer.invoke - 双向通信

双向通信适用于需要主进程处理并返回结果的场景,如文件选择、数据查询等。

基本用法

预加载脚本

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})

主进程

const { ipcMain, dialog } = require('electron')

ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
return null
})

渲染进程

const openFileBtn = document.getElementById('openFileBtn')
const filePathSpan = document.getElementById('filePath')

openFileBtn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
if (filePath) {
filePathSpan.textContent = filePath
}
})

完整示例:文件操作

预加载脚本

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
writeFile: (filePath, content) => ipcRenderer.invoke('file:write', filePath, content)
})

主进程

const { ipcMain } = require('electron')
const fs = require('node:fs/promises')

ipcMain.handle('file:read', async (event, filePath) => {
try {
const content = await fs.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
return { success: false, error: error.message }
}
})

ipcMain.handle('file:write', async (event, filePath, content) => {
try {
await fs.writeFile(filePath, content, 'utf-8')
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
})

渲染进程

async function loadFile() {
const result = await window.electronAPI.readFile('/path/to/file.txt')
if (result.success) {
document.getElementById('editor').value = result.content
} else {
alert('读取失败: ' + result.error)
}
}

async function saveFile() {
const content = document.getElementById('editor').value
const result = await window.electronAPI.writeFile('/path/to/file.txt', content)
if (result.success) {
alert('保存成功')
} else {
alert('保存失败: ' + result.error)
}
}

主进程向渲染进程发送消息

主进程可以主动向渲染进程发送消息,用于推送更新、通知事件等场景。

基本用法

主进程

const { BrowserWindow } = require('electron')

let mainWindow

function sendUpdate(data) {
if (mainWindow) {
mainWindow.webContents.send('update-data', data)
}
}

预加载脚本

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
onUpdateData: (callback) => ipcRenderer.on('update-data', (event, data) => callback(data))
})

渲染进程

window.electronAPI.onUpdateData((data) => {
console.log('收到更新:', data)
updateUI(data)
})

完整示例:进度通知

主进程

const { BrowserWindow, ipcMain } = require('electron')

let mainWindow

function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

ipcMain.handle('start-task', async () => {
for (let i = 0; i <= 100; i += 10) {
await new Promise(resolve => setTimeout(resolve, 500))
mainWindow.webContents.send('task-progress', i)
}
return { success: true }
})

预加载脚本

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
startTask: () => ipcRenderer.invoke('start-task'),
onProgress: (callback) => ipcRenderer.on('task-progress', (event, progress) => callback(progress))
})

渲染进程

const progressBar = document.getElementById('progressBar')
const startBtn = document.getElementById('startBtn')

window.electronAPI.onProgress((progress) => {
progressBar.value = progress
progressBar.textContent = progress + '%'
})

startBtn.addEventListener('click', async () => {
startBtn.disabled = true
await window.electronAPI.startTask()
startBtn.disabled = false
alert('任务完成')
})

IPC 安全最佳实践

通道白名单

不要直接暴露 ipcRenderer,而是使用白名单验证:

const { contextBridge, ipcRenderer } = require('electron')

const validSendChannels = ['set-title', 'save-file', 'open-file']
const validInvokeChannels = ['dialog:openFile', 'file:read', 'file:write']
const validReceiveChannels = ['update-data', 'task-progress']

contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
if (validSendChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
invoke: (channel, ...args) => {
if (validInvokeChannels.includes(channel)) {
return ipcRenderer.invoke(channel, ...args)
}
return Promise.reject(new Error('Invalid channel'))
},
on: (channel, callback) => {
if (validReceiveChannels.includes(channel)) {
const subscription = (event, ...args) => callback(...args)
ipcRenderer.on(channel, subscription)
return () => ipcRenderer.removeListener(channel, subscription)
}
}
})

类型安全的 API

为暴露的 API 添加类型定义:

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
writeFile: (filePath, content) => ipcRenderer.invoke('file:write', filePath, content)
})

TypeScript 类型定义:

interface ElectronAPI {
openFile: () => Promise<string | null>
readFile: (filePath: string) => Promise<{ success: boolean; content?: string; error?: string }>
writeFile: (filePath: string, content: string) => Promise<{ success: boolean; error?: string }>
}

declare global {
interface Window {
electronAPI: ElectronAPI
}
}

错误处理

在主进程中正确处理错误:

ipcMain.handle('file:read', async (event, filePath) => {
try {
if (!filePath || typeof filePath !== 'string') {
throw new Error('Invalid file path')
}

const content = await fs.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
console.error('读取文件失败:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
})

高级用法

多窗口通信

当应用有多个窗口时,可以向特定窗口发送消息:

const { BrowserWindow } = require('electron')

let settingsWindow
let mainWindow

function sendToWindow(window, channel, data) {
if (window && !window.isDestroyed()) {
window.webContents.send(channel, data)
}
}

function broadcastToAllWindows(channel, data) {
const windows = BrowserWindow.getAllWindows()
windows.forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send(channel, data)
}
})
}

消息确认机制

实现消息确认机制,确保消息被正确处理:

ipcMain.handle('save-data', async (event, data) => {
try {
await saveToDatabase(data)
return { success: true, message: '保存成功' }
} catch (error) {
return { success: false, message: error.message }
}
})

流式数据传输

对于大量数据,可以使用流式传输:

const { ipcMain } = require('electron')
const fs = require('node:fs')

ipcMain.handle('read-large-file', async (event, filePath) => {
const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 })
const webContents = event.sender

return new Promise((resolve, reject) => {
stream.on('data', (chunk) => {
webContents.send('file-chunk', chunk.toString())
})

stream.on('end', () => {
webContents.send('file-end')
resolve({ success: true })
})

stream.on('error', (error) => {
reject({ success: false, error: error.message })
})
})
})

常见问题

消息丢失

确保在发送消息前窗口已经加载完成:

mainWindow.webContents.on('did-finish-load', () => {
mainWindow.webContents.send('init-data', initialData)
})

内存泄漏

移除不再需要的监听器:

contextBridge.exposeInMainWorld('electronAPI', {
onUpdate: (callback) => {
const handler = (event, data) => callback(data)
ipcRenderer.on('update', handler)
return () => ipcRenderer.removeListener('update', handler)
}
})

const unsubscribe = window.electronAPI.onUpdate(handleUpdate)
// 不再需要时
unsubscribe()

调试 IPC

在主进程中记录所有 IPC 通信:

const originalOn = ipcMain.on.bind(ipcMain)
ipcMain.on = (channel, listener) => {
const wrappedListener = (event, ...args) => {
console.log(`[IPC] 收到消息: ${channel}`, args)
listener(event, ...args)
}
return originalOn(channel, wrappedListener)
}

小结

本章介绍了 Electron 进程间通信的核心内容:

  1. 单向通信:使用 ipcRenderer.sendipcMain.on
  2. 双向通信:使用 ipcRenderer.invokeipcMain.handle
  3. 主进程推送:使用 webContents.send
  4. 安全实践:通道白名单、类型安全、错误处理
  5. 高级用法:多窗口通信、消息确认、流式传输

正确使用 IPC 是开发安全、高效 Electron 应用的关键。