主题切换
第 7 章:进程通信与数据持久化
本章在第 2 章 IPC 基础之上,深入讲解 Electron 进程间通信的进阶话题以及应用数据的持久化方案。第 2 章已覆盖三种 IPC 通信模式(send/on、invoke/handle、webContents.send)和 contextBridge 安全机制——如果你尚未阅读,建议先回顾第 2 章 IPC 部分。
本章将聚焦:同步/异步通信的陷阱、大数据传输优化、主进程推送的流式模式、IPC 错误处理策略、以及 electron-store 和 SQLite 两种数据持久化方案。
7.1 IPC 通信基础
Electron 的 IPC 系统类似于前端开发中的"客户端-服务器"通信模型:主进程是"服务器",渲染进程是"客户端", 它们通过预定义的"频道"(channel)进行消息传递。
给前端开发者的建议
把 ipcMain 想象成 Express 路由处理器,ipcRenderer 就是前端发 HTTP 请求的 fetch/axios。只不过这里的"网络"是进程间内存通道,速度极快。
通信模式对比
| 模式 | 方向 | 特点 | 适用场景 |
|---|---|---|---|
| 渲染 → 主进程 | 单向 | fire-and-forget | 触发操作、发送事件 |
| 渲染 → 主进程(invoke) | 双向 | Promise 返回值 | 请求数据、执行并等待结果 |
| 主进程 → 渲染 | 单向 | 通过 webContents | 推送通知、广播事件 |
渲染进程 → 主进程(单向)
javascript
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();
});javascript
// 发送消息到主进程(无需等待回复)
function minimizeWindow() {
ipcRenderer.send('app:minimize');
}
function maximizeWindow() {
ipcRenderer.send('app:maximize');
}
function closeWindow() {
ipcRenderer.send('app:close');
}渲染进程 → 主进程(双向 invoke)
javascript
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
};
});javascript
// 使用 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);
}
}主进程 → 渲染进程(推送)
javascript
// 向指定窗口发送消息
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
});
}javascript
// 监听主进程推送的消息
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。
IPC 调用超时处理
为 invoke 添加超时保护,防止主进程处理挂起导致渲染进程无限等待:
javascript
// 安全的 invoke 超时封装
async function invokeWithTimeout(channel, args, timeout = 10000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`IPC 调用超时 (${channel})`)), timeout)
)
return Promise.race([
ipcRenderer.invoke(channel, args),
timeoutPromise
])
}
// 使用
try {
const result = await invokeWithTimeout('file:read', filePath, 5000)
console.log('文件内容:', result)
} catch (err) {
console.error('IPC 调用失败:', err.message)
}主进程推送的流式数据模式
对于下载进度、日志流等需要持续推送数据的场景:
javascript
const { ipcMain } = require('electron')
// 根据事件发送者获取窗口
ipcMain.handle('task:execute', async (event, taskId) => {
const win = BrowserWindow.fromWebContents(event.sender)
// 通过 webContents.send 持续推送进度
for (let i = 0; i <= 100; i += 10) {
await new Promise(r => setTimeout(r, 500)) // 模拟耗时操作
win.webContents.send('task:progress', { taskId, progress: i })
}
return { success: true, taskId }
})javascript
// 渲染进程监听进度推送
let progressHandler = null
function executeTask(taskId) {
// 先注册进度监听
progressHandler = (event, data) => {
if (data.taskId === taskId) {
updateProgressBar(data.progress)
}
}
ipcRenderer.on('task:progress', progressHandler)
// 发起任务
return ipcRenderer.invoke('task:execute', taskId)
.finally(() => {
// 任务完成后移除监听
ipcRenderer.removeListener('task:progress', progressHandler)
})
}7.3 使用 contextBridge 安全暴露 API
contextBridge 是 Electron 推荐的安全 API 暴露方式。它在渲染进程的 JavaScript 上下文和预加载脚本之间建立一座安全的"桥梁"。
安全模型
把 contextBridge 想象成 API 网关:预加载脚本拥有完整的 Node.js 能力,但它只向渲染进程暴露经过筛选和封装的 API,渲染进程无法直接访问任何 Node.js 模块。
javascript
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);
}
});javascript
// 在 Vue 组件中通过 window.electronAPI 访问
import { ref, onMounted, onUnmounted } from 'vue';
const isMaximized = ref(false);
const downloadProgress = ref(0);
// 窗口控制
function handleMinimize() {
window.electronAPI.window.minimize();
}
function handleMaximize() {
window.electronAPI.window.maximize();
}
// 文件操作
async function handleOpenFile() {
const result = await window.electronAPI.file.open({
filters: [{ name: '文本文件', extensions: ['txt'] }]
});
if (!result.canceled) {
console.log('文件内容:', result.content);
}
}
// 监听下载进度
function onDownloadProgress(event, data) {
downloadProgress.value = data.percent;
}
onMounted(() => {
window.electronAPI.on('download:progress', onDownloadProgress);
});
onUnmounted(() => {
window.electronAPI.off('download:progress', onDownloadProgress);
});7.4 大数据传输优化
当需要在进程间传输大量数据(如文件内容、图片二进制)时,直接通过 IPC 传输可能导致性能问题。 以下是几种优化方案:
方案一:传输文件路径而非内容
javascript
// ❌ 不要这样做 - 传输大文件内容
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);
// 使用流处理,不一次性加载到内存
});方案二:使用流式传输
javascript
// 分块读取大文件
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
bash
npm install electron-store@6⚠️ 版本兼容性提示
electron-store v8 及以上是纯 ESM 模块,与 CJS 构建格式(formats: ['cjs'])不兼容。在本教程的 electron-vite 配置中,建议安装 v6 版本(npm install electron-store@6)。如果使用 v8+,需改用动态 import()。
javascript
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)
javascript
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')
}
});javascript
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 应用配置管理
一个好的配置管理系统应该支持类型安全、默认值、验证和变更监听。以下是一个完整的配置管理实现:
javascript
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 应用应该将日志写入文件,而不是仅仅输出到控制台。
bash
npm install electron-logjavascript
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 实现跨进程状态同步。
javascript
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
};
});本章小结
本章深入讲解了 Electron 进程通信与数据持久化的核心知识:
- IPC 三种模式:
send/on(单向推送)、invoke/handle(请求-响应)、webContents.send(主到渲),根据场景选择合适的方式 - 安全性:通过
contextBridge暴露白名单 API,使用domain:action命名规范组织 IPC 通道,所有渲染进程输入都需要在主进程端校验 - 数据持久化:
electron-store适合配置和简单键值数据(自动持久化 JSON),SQLite(better-sqlite3)适合结构化数据和复杂查询场景 - 日志系统:
electron-log提供分级日志,建议结合 Sentry 实现生产环境的错误追踪 - Pinia 状态管理:跨组件状态共享,配合
electron-store实现持久化插件
最佳实践总结
- IPC 频道命名:使用
模块:动作格式(如dialog:openFile),避免命名冲突 - 错误处理:所有
invoke调用都要用 try-catch 包裹 - 内存管理:组件卸载时移除 IPC 事件监听,避免内存泄漏
- 数据验证:主进程接收数据时进行校验,防止恶意输入
- 性能优化:大数据传输使用文件路径或流,避免直接传输大体积二进制数据
7.6 Web Workers 与多线程
对于 CPU 密集型任务(如图像处理、大数据计算、复杂加密),应避免阻塞主进程或渲染进程。Electron 支持使用 Node.js 的 worker_threads 或标准 Web Workers。
使用 Node.js worker_threads
在主进程中使用 worker_threads 执行耗时任务:
javascript
// main.js
const { Worker } = require('worker_threads')
function runHeavyTask(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./workers/image-processor.js')
worker.postMessage(data)
worker.on('message', resolve)
worker.on('error', reject)
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker 退出码: ${code}`))
})
})
}
// workers/image-processor.js
const { parentPort } = require('worker_threads')
parentPort.on('message', async (imageData) => {
// 执行耗时操作(如 OCR、图像压缩)
const result = await processImage(imageData)
parentPort.postMessage(result)
})在渲染进程中使用 Web Workers
渲染进程中可直接使用标准 Web Workers,但需注意 CSP 配置:
javascript
// renderer.js
const worker = new Worker('./workers/data-analyzer.js')
worker.postMessage({ type: 'ANALYZE', payload: largeDataSet })
worker.onmessage = (event) => {
console.log('分析结果:', event.data)
}
// workers/data-analyzer.js
self.onmessage = (event) => {
const { type, payload } = event.data
if (type === 'ANALYZE') {
const result = performAnalysis(payload)
self.postMessage(result)
}
}Electron 中 Worker 的注意事项
| 类型 | 可用环境 | 可访问 API | 适用场景 |
|---|---|---|---|
worker_threads | 主进程 | Node.js 完整 API | 文件处理、原生模块调用 |
| Web Workers | 渲染进程 | 浏览器 API + 部分 Node.js | 数据计算、UI 不阻塞 |
SharedWorker | 渲染进程 | 同 Web Workers | 多页面共享状态 |
何时使用多线程
- 图像/视频处理:OCR、压缩、格式转换
- 数据分析:CSV 解析、大数据聚合
- 加密运算:密码哈希、文件校验
- RPA 自动化:复杂逻辑判断、屏幕分析
注意事项
- Worker 中无法直接访问 DOM 或 Electron 的 contextBridge API
- 主线程与 Worker 间传输数据使用结构化克隆算法,不能传递函数或循环引用
- 使用
worker.terminate()或worker.unref()确保 Worker 正确退出,避免内存泄漏
下一章我们将学习应用打包与跨平台发布,将开发好的应用分发给用户。