第 7 章:进程通信与数据持久化

本章深入讲解 Electron 最核心的架构机制——进程间通信(IPC),以及应用数据的持久化方案。 理解 IPC 是掌握 Electron 的关键,它决定了你的应用如何安全、高效地在主进程和渲染进程之间传递数据和调用功能。

7.1 IPC 通信基础

Electron 的 IPC 系统类似于前端开发中的"客户端-服务器"通信模型:主进程是"服务器",渲染进程是"客户端", 它们通过预定义的"频道"(channel)进行消息传递。

💡给前端开发者的建议

ipcMain 想象成 Express 路由处理器,ipcRenderer 就是前端发 HTTP 请求的 fetch/axios。 只不过这里的"网络"是进程间内存通道,速度极快。

通信模式对比

模式方向特点适用场景
渲染 → 主进程单向fire-and-forget触发操作、发送事件
渲染 → 主进程(invoke)双向Promise 返回值请求数据、执行并等待结果
主进程 → 渲染单向通过 webContents推送通知、广播事件

渲染进程 → 主进程(单向)

main.js
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();
});
renderer.js
// 发送消息到主进程(无需等待回复)
function minimizeWindow() {
  ipcRenderer.send('app:minimize');
}

function maximizeWindow() {
  ipcRenderer.send('app:maximize');
}

function closeWindow() {
  ipcRenderer.send('app:close');
}

渲染进程 → 主进程(双向 invoke)

main.js
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
  };
});
renderer.js
// 使用 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);
  }
}

主进程 → 渲染进程(推送)

main.js
// 向指定窗口发送消息
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
  });
}
renderer.js
// 监听主进程推送的消息
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 模块。

preload.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 组件中使用
// 在 Vue 组件中通过 window.electronAPI 访问

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()
store.js
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)

preload.js
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')
  }
});
main.js
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 应用配置管理

一个好的配置管理系统应该支持类型安全、默认值、验证和变更监听。以下是一个完整的配置管理实现:

config-manager.js
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-log
logger.js
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 实现跨进程状态同步。

stores/app.js
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 事件监听,避免内存泄漏
  • 数据验证:主进程接收数据时进行校验,防止恶意输入
  • 性能优化:大数据传输使用文件路径或流,避免直接传输二进制