进程间通信(IPC)
进程间通信(Inter-Process Communication,IPC)是 Electron 应用的核心机制。由于主进程和渲染进程运行在独立的进程中,它们无法直接共享数据,必须通过 IPC 进行通信。本章将详细介绍 IPC 的工作原理和最佳实践。
IPC 基本概念
为什么需要 IPC
Electron 采用多进程架构,主进程和渲染进程相互隔离:
- 主进程:拥有完整的 Node.js 能力,可以访问文件系统、调用原生 API
- 渲染进程:运行 Web 环境,默认无法访问 Node.js
这种隔离设计提高了安全性,但也带来了进程间数据交换的需求。IPC 就是连接这两个进程的桥梁。
IPC 通信方向
IPC 支持三种通信方向:
- 渲染进程 → 主进程(单向):渲染进程发送消息,不等待响应
- 渲染进程 → 主进程(双向):渲染进程发送消息,等待主进程返回结果
- 主进程 → 渲染进程:主进程主动向渲染进程发送消息
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 进程间通信的核心内容:
- 单向通信:使用
ipcRenderer.send和ipcMain.on - 双向通信:使用
ipcRenderer.invoke和ipcMain.handle - 主进程推送:使用
webContents.send - 安全实践:通道白名单、类型安全、错误处理
- 高级用法:多窗口通信、消息确认、流式传输
正确使用 IPC 是开发安全、高效 Electron 应用的关键。