第 7 章:进程通信与数据持久化
本章深入讲解 Electron 最核心的架构机制——进程间通信(IPC),以及应用数据的持久化方案。 理解 IPC 是掌握 Electron 的关键,它决定了你的应用如何安全、高效地在主进程和渲染进程之间传递数据和调用功能。
7.1 IPC 通信基础
Electron 的 IPC 系统类似于前端开发中的"客户端-服务器"通信模型:主进程是"服务器",渲染进程是"客户端", 它们通过预定义的"频道"(channel)进行消息传递。
把 ipcMain 想象成 Express 路由处理器,ipcRenderer 就是前端发 HTTP 请求的 fetch/axios。
只不过这里的"网络"是进程间内存通道,速度极快。
通信模式对比
| 模式 | 方向 | 特点 | 适用场景 |
|---|---|---|---|
| 渲染 → 主进程 | 单向 | fire-and-forget | 触发操作、发送事件 |
| 渲染 → 主进程(invoke) | 双向 | Promise 返回值 | 请求数据、执行并等待结果 |
| 主进程 → 渲染 | 单向 | 通过 webContents | 推送通知、广播事件 |
渲染进程 → 主进程(单向)
const { ipcMain } = require('electron');
// 监听来自渲染进程的消息
ipcMain.on('app:minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.minimize();
});
ipcMain.on('app:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
});
ipcMain.on('app:close', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.close();
});
// 发送消息到主进程(无需等待回复)
function minimizeWindow() {
ipcRenderer.send('app:minimize');
}
function maximizeWindow() {
ipcRenderer.send('app:maximize');
}
function closeWindow() {
ipcRenderer.send('app:close');
}
渲染进程 → 主进程(双向 invoke)
const { ipcMain, dialog } = require('electron');
const fs = require('fs').promises;
// 处理带返回值的请求
ipcMain.handle('dialog:openFile', async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win, {
properties: ['openFile'],
filters: options.filters || [{ name: '所有文件', extensions: ['*'] }]
});
if (result.canceled) {
return { canceled: true };
}
const content = await fs.readFile(result.filePaths[0], 'utf-8');
return {
canceled: false,
filePath: result.filePaths[0],
content
};
});
// 使用 invoke 发送请求并等待结果
async function openAndReadFile() {
try {
const result = await ipcRenderer.invoke('dialog:openFile', {
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] }
]
});
if (!result.canceled) {
console.log('文件路径:', result.filePath);
console.log('文件内容:', result.content.substring(0, 100));
return result;
}
} catch (error) {
console.error('操作失败:', error);
}
}
主进程 → 渲染进程(推送)
// 向指定窗口发送消息
function sendToRenderer(win, channel, ...args) {
win.webContents.send(channel, ...args);
}
// 广播给所有窗口
function broadcast(channel, ...args) {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send(channel, ...args);
});
}
// 使用示例:下载进度推送
function notifyDownloadProgress(win, progress) {
sendToRenderer(win, 'download:progress', {
percent: progress.percent,
transferred: progress.transferred,
total: progress.total
});
}
// 监听主进程推送的消息
ipcRenderer.on('download:progress', (event, data) => {
console.log(`下载进度: ${data.percent}%`);
updateProgressBar(data.percent);
});
ipcRenderer.on('theme:changed', (event, theme) => {
document.body.setAttribute('data-theme', theme);
});
7.2 同步 vs 异步通信
Electron 支持同步 IPC 通信,但强烈不建议使用。同步通信会阻塞渲染进程,导致界面卡顿。
| 特性 | 同步(sendSync) | 异步(invoke) |
|---|---|---|
| 返回值 | 直接返回 | Promise |
| 界面阻塞 | 会阻塞渲染进程 | 不阻塞 |
| 错误处理 | 抛出异常 | Promise reject |
| 推荐使用 | 仅在初始化配置读取 | 所有场景 |
ipcRenderer.sendSync 会冻结整个渲染进程直到主进程响应。如果主进程繁忙或死锁,你的应用界面将完全卡死。
永远优先使用 invoke。
7.3 使用 contextBridge 安全暴露 API
contextBridge 是 Electron 推荐的安全 API 暴露方式。它在渲染进程的 JavaScript 上下文和预加载脚本之间建立一座安全的"桥梁"。
把 contextBridge 想象成 API 网关:预加载脚本拥有完整的 Node.js 能力,但它只向渲染进程暴露经过筛选和封装的 API,
渲染进程无法直接访问任何 Node.js 模块。
const { contextBridge, ipcRenderer } = require('electron');
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口控制
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
isMaximized: () => ipcRenderer.invoke('window:isMaximized')
},
// 文件操作
file: {
open: (options) => ipcRenderer.invoke('dialog:openFile', options),
save: (options) => ipcRenderer.invoke('dialog:saveFile', options),
read: (path) => ipcRenderer.invoke('file:read', path),
write: (path, content) => ipcRenderer.invoke('file:write', path, content)
},
// 系统信息
system: {
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getPlatform: () => process.platform,
getPath: (name) => ipcRenderer.invoke('app:getPath', name)
},
// 事件监听(只允许白名单频道)
on: (channel, callback) => {
const validChannels = [
'download:progress',
'theme:changed',
'app:update-available'
];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, callback);
}
},
// 移除事件监听
off: (channel, callback) => {
ipcRenderer.removeListener(channel, callback);
}
});
// 在 Vue 组件中通过 window.electronAPI 访问
7.4 大数据传输优化
当需要在进程间传输大量数据(如文件内容、图片二进制)时,直接通过 IPC 传输可能导致性能问题。 以下是几种优化方案:
方案一:传输文件路径而非内容
// ❌ 不要这样做 - 传输大文件内容
const largeFileContent = fs.readFileSync('./video.mp4'); // 几百MB
ipcRenderer.send('process-video', largeFileContent); // 阻塞 IPC!
// ✅ 正确做法 - 只传输路径
const filePath = './video.mp4';
ipcRenderer.send('process-video', { filePath }); // 只传字符串
// 主进程中读取文件
ipcMain.on('process-video', (event, { filePath }) => {
const stream = fs.createReadStream(filePath);
// 使用流处理,不一次性加载到内存
});
方案二:使用流式传输
// 分块读取大文件
async function* readFileChunks(filePath, chunkSize = 64 * 1024) {
const fd = await fs.open(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
try {
while (true) {
const { bytesRead } = await fd.read(buffer, 0, chunkSize, null);
if (bytesRead === 0) break;
yield buffer.slice(0, bytesRead);
}
} finally {
await fd.close();
}
}
// 主进程中分块处理
ipcMain.handle('process-large-file', async (event, filePath) => {
for await (const chunk of readFileChunks(filePath)) {
// 处理每一块数据
event.sender.send('file:chunk', chunk.toString('base64'));
}
event.sender.send('file:complete');
});
7.5 数据持久化方案
Electron 应用需要保存用户配置、应用状态等数据。根据数据类型和规模,可以选择不同的持久化方案。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| localStorage | 小型配置、临时数据 | 简单易用 | 容量限制(~5MB),仅渲染进程可用 |
| IndexedDB | 结构化数据、离线缓存 | 容量大、支持索引 | API复杂、仅渲染进程可用 |
| JSON文件 | 应用配置、用户设置 | 简单直观、可手动编辑 | 不适合大量数据 |
| SQLite | 关系型数据、大量记录 | 轻量、支持SQL | 需要额外依赖 |
| electron-store | 应用配置 | 专为Electron设计、自动序列化 | 功能较简单 |
使用 electron-store
npm install electron-store@6
electron-store v8 及以上是纯 ESM 模块,与 CJS 构建格式(formats: ['cjs'])不兼容。在本教程的 electron-vite 配置中,建议安装 v6 版本(npm install electron-store@6)。如果使用 v8+,需改用动态 import()。
const Store = require('electron-store');
// 创建存储实例
const store = new Store({
name: 'app-config', // 配置文件名
defaults: {
theme: 'dark',
language: 'zh-CN',
windowBounds: {
width: 1200,
height: 800
},
recentFiles: [],
settings: {
autoSave: true,
saveInterval: 30000
}
}
});
// 读取配置
const theme = store.get('theme');
const windowBounds = store.get('windowBounds');
// 设置配置
store.set('theme', 'light');
store.set('windowBounds.width', 1400);
// 删除配置
store.delete('recentFiles');
// 清空所有配置
store.clear();
// 获取配置存储路径
console.log('配置文件位置:', store.path);
module.exports = { store };
在渲染进程中使用(通过 Preload)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
store: {
get: (key) => ipcRenderer.invoke('store:get', key),
set: (key, value) => ipcRenderer.invoke('store:set', key, value),
delete: (key) => ipcRenderer.invoke('store:delete', key),
clear: () => ipcRenderer.invoke('store:clear')
}
});
const { ipcMain } = require('electron');
const { store } = require('./store');
ipcMain.handle('store:get', (event, key) => {
return store.get(key);
});
ipcMain.handle('store:set', (event, key, value) => {
store.set(key, value);
});
ipcMain.handle('store:delete', (event, key) => {
store.delete(key);
});
ipcMain.handle('store:clear', () => {
store.clear();
});
7.6 应用配置管理
一个好的配置管理系统应该支持类型安全、默认值、验证和变更监听。以下是一个完整的配置管理实现:
const Store = require('electron-store');
const { ipcMain } = require('electron');
class ConfigManager {
constructor() {
this.store = new Store({
name: 'app-config',
schema: {
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
default: 'dark'
},
language: {
type: 'string',
default: 'zh-CN'
},
autoUpdate: {
type: 'boolean',
default: true
},
shortcuts: {
type: 'object',
default: {
screenshot: 'CommandOrControl+Shift+S',
newTask: 'CommandOrControl+N'
}
}
}
});
this.listeners = new Map();
this.setupIPC();
}
get(key, defaultValue) {
return this.store.get(key, defaultValue);
}
set(key, value) {
const oldValue = this.store.get(key);
this.store.set(key, value);
this.emit(key, value, oldValue);
return value;
}
onChange(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(callback);
return () => this.listeners.get(key)?.delete(callback);
}
emit(key, newValue, oldValue) {
const callbacks = this.listeners.get(key);
if (callbacks) {
callbacks.forEach(cb => {
try {
cb(newValue, oldValue);
} catch (err) {
console.error('配置监听器错误:', err);
}
});
}
}
setupIPC() {
ipcMain.handle('config:get', (event, key) => this.get(key));
ipcMain.handle('config:set', (event, key, value) => this.set(key, value));
ipcMain.handle('config:get-all', () => this.store.store);
}
}
module.exports = { ConfigManager };
7.7 日志系统实现
生产环境的应用需要完善的日志系统来追踪问题。Electron 应用应该将日志写入文件,而不是仅仅输出到控制台。
npm install electron-log
const log = require('electron-log');
const path = require('path');
const { app } = require('electron');
// 配置日志
log.transports.file.resolvePath = () => {
return path.join(app.getPath('logs'), 'app.log');
};
log.transports.file.level = 'info';
log.transports.console.level = 'debug';
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
// 封装日志 API
const logger = {
debug: (message, ...args) => log.debug(message, ...args),
info: (message, ...args) => log.info(message, ...args),
warn: (message, ...args) => log.warn(message, ...args),
error: (message, ...args) => log.error(message, ...args)
};
// 捕获未处理的错误
process.on('uncaughtException', (error) => {
logger.error('未捕获的异常:', error);
});
process.on('unhandledRejection', (reason) => {
logger.error('未处理的 Promise 拒绝:', reason);
});
module.exports = { logger };
7.8 状态管理(Pinia 在 Electron 中的使用)
在 Electron 中使用 Pinia 管理应用状态,并结合 IPC 实现跨进程状态同步。
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useAppStore = defineStore('app', () => {
// State
const theme = ref('dark');
const isMaximized = ref(false);
const recentFiles = ref([]);
// Getters
const isDark = computed(() => theme.value === 'dark');
// Actions
async function setTheme(newTheme) {
theme.value = newTheme;
// 持久化到主进程
await window.electronAPI.store.set('theme', newTheme);
// 通知其他窗口
window.electronAPI.window.broadcast('theme:changed', newTheme);
}
async function loadSettings() {
const saved = await window.electronAPI.store.get('theme');
if (saved) theme.value = saved;
const files = await window.electronAPI.store.get('recentFiles');
if (files) recentFiles.value = files;
}
function addRecentFile(filePath) {
recentFiles.value = [filePath, ...recentFiles.value.filter(f => f !== filePath)].slice(0, 10);
window.electronAPI.store.set('recentFiles', recentFiles.value);
}
return {
theme, isMaximized, recentFiles,
isDark,
setTheme, loadSettings, addRecentFile
};
});
- IPC 频道命名:使用
模块:动作格式(如dialog:openFile),避免命名冲突 - 错误处理:所有 invoke 调用都要用 try-catch 包裹
- 内存管理:组件卸载时移除 IPC 事件监听,避免内存泄漏
- 数据验证:主进程接收数据时进行校验,防止恶意输入
- 性能优化:大数据传输使用文件路径或流,避免直接传输二进制