electron

IPC 通信机制详解

By AI-Writer 10 min read

前言

Electron 的主进程与渲染进程运行在不同的 JavaScript 上下文中,它们不能直接相互调用,必须通过 IPC(Inter-Process Communication,进程间通信)机制交换数据和命令。IPC 是 Electron 架构中最重要的机制之一,本文将全面讲解其使用方法和最佳实践。

IPC 模块概览

Electron 提供了两套 IPC API:

API用途特点
ipcMain主进程接收消息主进程端
ipcRenderer渲染进程发送消息渲染进程端
webContents主进程主动发消息给渲染进程主进程端

通信模式分为两种:请求-响应模式(invoke/handle)和事件模式(send/on)。

invoke / handle:双向异步请求

基本用法

这是 Electron 推荐的首选通信模式,基于 Promise,支持双向异步通信:

typescript
// main.ts — 主进程:注册处理程序
import { ipcMain } from 'electron';

// 注册处理程序,channel 建议使用 'domain:action' 命名
ipcMain.handle('file:read', async (event, filePath: string) => {
  const fs = require('fs');
  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    return { success: true, data: content };
  } catch (error) {
    return { success: false, error: (error as Error).message };
  }
});

// 处理程序中禁止使用 event.sender
// 正确做法是通过返回值传递数据
typescript
// renderer.ts — 渲染进程:调用处理程序
import { ipcRenderer } from 'electron';

async function readConfig() {
  const result = await ipcRenderer.invoke('file:read', './config.json');
  if (result.success) {
    console.log('文件内容:', result.data);
  } else {
    console.error('读取失败:', result.error);
  }
}

为什么推荐 invoke/handle

  • 基于 Promise,代码更清晰,避免回调地狱
  • 主进程返回的值直接作为 Promise 的 resolve 值
  • 支持超时、取消等高级特性

错误处理

typescript
// main.ts — 抛出错误
ipcMain.handle('db:query', async (event, sql: string) => {
  if (!sql) {
    throw new Error('SQL 语句不能为空');
  }
  return db.query(sql);
});

// renderer.ts — 捕获错误
try {
  const result = await ipcRenderer.invoke('db:query', 'SELECT * FROM users');
} catch (error) {
  console.error('查询失败:', (error as Error).message);
}

send / on:事件监听模式

渲染进程向主进程发送消息(fire-and-forget)

适用于不需要返回值的场景,如日志上报、用户行为统计:

typescript
// renderer.ts
function trackEvent(eventName: string, properties: Record<string, unknown>) {
  ipcRenderer.send('analytics:track', eventName, properties);
}

// 主进程监听
ipcMain.on('analytics:track', (event, eventName, properties) => {
  console.log('用户行为:', eventName, properties);
  // 发送到分析服务
  sendToAnalytics(eventName, properties);
});

注意send 是单向的,渲染进程不知道主进程是否收到。

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

应用场景:主进程检测到系统事件(如网络状态变化),通知渲染进程更新 UI:

typescript
// main.ts
function notifyRenderer(status: 'online' | 'offline') {
  const windows = BrowserWindow.getAllWindows();
  windows.forEach(win => {
    win.webContents.send('network:status-change', status);
  });
}

// 监听系统网络变化(示例)
import { net } from 'electron';
net.on('online', () => notifyRenderer('online'));
net.on('offline', () => notifyRenderer('offline'));
typescript
// renderer.ts
ipcRenderer.on('network:status-change', (event, status) => {
  updateNetworkIndicator(status); // 更新 UI
});

// 组件卸载时移除监听器,防止内存泄漏
import { onUnmounted } from 'vue';

onMounted(() => {
  const handler = (_event: Electron.IpcRendererEvent, status: string) => {
    updateNetworkIndicator(status);
  };
  ipcRenderer.on('network:status-change', handler);

  onUnmounted(() => {
    ipcRenderer.removeListener('network:status-change', handler);
  });
});

channel 命名规范

良好的 channel 命名能避免冲突,便于维护:

typescript
// 命名规范:'模块:操作'
// 模块 = 功能域(大写驼峰或 kebab-case)
// 操作 = 具体动作(动词)

// ✅ 推荐
ipcMain.handle('file:read', ...);
ipcMain.handle('file:write', ...);
ipcMain.on('analytics:track', ...);
ipcMain.handle('window:minimize', ...);

// ❌ 不推荐(易冲突,难以定位)
ipcMain.handle('read', ...);
ipcMain.handle('readFile', ...);
ipcMain.on('data', ...);

进程间数据传输规则

可序列化的数据

以下数据可以通过 IPC 传递:

  • 基本类型:stringnumberbooleannullundefined
  • 数组和对象(JSON 可序列化)
  • 满足特定条件的对象(拥有 toJSON 方法)

不能直接传递的数据

typescript
// ❌ 这些不能直接通过 IPC 传递
ipcRenderer.send('window:set-title', windowInstance);  // DOM 节点
ipcRenderer.send('file:process', fileBuffer);          // 原生 Buffer(需转换)
ipcRenderer.send('app:send', classInstance);           // 类实例

// ✅ 正确做法:传递可序列化数据
ipcRenderer.send('window:set-title', { title: '新标题' });
ipcRenderer.send('file:process', { path: '/path/to/file' });

传输 Buffer 和文件

typescript
// main.ts — 读取文件后转为 Base64 传递
import { readFile } from 'fs/promises';
import { Buffer } from 'buffer';

ipcMain.handle('file:get-image', async (event, filePath: string) => {
  const buffer = await readFile(filePath);
  // 将 Buffer 转为 Base64(可序列化)
  return buffer.toString('base64');
});

// renderer.ts
const base64 = await ipcRenderer.invoke('file:get-image', '/path/to/image.png');
const img = new Image();
img.src = `data:image/png;base64,${base64}`;
document.body.appendChild(img);

使用 File 对象的正确方式

拖拽文件到 Electron 窗口时,获取的是 File 对象,它包含 path 属性指向本地路径:

typescript
// renderer.ts — 拖拽文件
dropZone.addEventListener('drop', async (e) => {
  const files = e.dataTransfer?.files;
  if (files && files.length > 0) {
    const filePath = (files[0] as File & { path: string }).path;
    // 通过 IPC 传递文件路径(不是 File 对象)
    await ipcRenderer.invoke('file:process', filePath);
  }
});

返回值与 Promise 链

invoke/handle 的返回值支持链式调用:

typescript
// main.ts — 可以返回任何可序列化的值
ipcMain.handle('config:get-all', async () => {
  return {
    theme: 'dark',
    language: 'zh-CN',
    user: { id: 1, name: 'Alice' },
  };
});

// renderer.ts — 链式使用
const config = await ipcRenderer.invoke('config:get-all');
const lang = config.language;

主进程主动发消息给渲染进程

webContents.send 外,还可以使用 WebContents 的事件:

typescript
// main.ts
const win = new BrowserWindow({ width: 800, height: 600 });

// 等渲染进程加载完成后发送
win.webContents.on('did-finish-load', () => {
  win.webContents.send('init:data', { userId: 123 });
});

// 网络进度通知
win.webContents.on('did-start-loading', () => showLoading());
win.webContents.on('did-stop-loading', () => hideLoading());

常见陷阱

监听器未移除导致内存泄漏

typescript
// ❌ 错误:多次挂载同一监听器
onMounted(() => {
  ipcRenderer.on('data:update', handler);
  // 组件重新挂载时,handler 会被注册多次
});

// ✅ 正确:使用 once 或在 onUnmounted 中清理
ipcRenderer.once('data:update', handler);  // 只监听一次

// 或
onUnmounted(() => {
  ipcRenderer.removeListener('data:update', handler);
});

contextIsolation 下的 event.sender

contextIsolation: true 时,event.sender 不可用:

typescript
// ❌ contextIsolation: true 时,以下代码无效
ipcMain.handle('user:get-info', (event) => {
  const sender = event.sender; // 被禁用,访问会报错
});

// ✅ 通过 preload 暴露的 contextBridge 访问
// 详见下一篇文章「Preload 脚本与上下文隔离」

小结

  • invoke/handle 是 Electron IPC 的首选模式,基于 Promise,支持异步返回值
  • send/on 适用于单向通知或 fire-and-forget 场景
  • channel 命名推荐 '模块:操作' 格式,避免冲突
  • 渲染进程通过 IPC 传递可序列化数据,Buffer 等使用 Base64 转换
  • 始终在组件卸载时移除 IPC 监听器,防止内存泄漏

下一篇文章我们将深入 Preload 脚本与上下文隔离,学习如何通过 contextBridge 安全地暴露 API,以及如何正确配置 nodeIntegrationcontextIsolation

#electron #ipc #进程通信 #contextBridge

评论

A

Written by

AI-Writer

Related Articles

electron
#4

Preload 脚本与上下文隔离

理解 Preload 脚本的执行时机、contextBridge 安全暴露 API、nodeIntegration 与 contextIsolation 配置组合,以及主进程与渲染进程的安全边界

Read More
electron
#7

Node.js 原生模块调用

在 Electron 主进程中使用 npm 原生模块(sqlite3、sharp),Native Module 编译(node-gyp / electron-rebuild),纯 JS 替代方案以及多线程 Worker Threads 实践

Read More
electron
#3

IPC 通信机制详解

深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则

Read More