Skip to content

Electron 核心架构

主进程、渲染进程与 IPC 通信机制

目录


进程模型

两类进程

┌─────────────────────────────────────────────────────────────┐
│                     Electron 应用                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────────┐                                    │
│   │    Main Process     │  ← 唯一,Node.js 环境              │
│   │                     │                                    │
│   │  • 应用生命周期管理   │                                    │
│   │  • 创建 BrowserWindow │                                   │
│   │  • 系统 API 调用     │                                    │
│   │  • 原生菜单/托盘     │                                    │
│   └──────────┬──────────┘                                    │
│              │ IPC                                           │
│   ┌──────────┴──────────┐                                    │
│   │                     │                                    │
│   ▼                     ▼                                    │
│ ┌───────────────┐ ┌───────────────┐                          │
│ │Renderer Process│ │Renderer Process│  ← 多个,Chromium 环境  │
│ │  (Window 1)   │ │  (Window 2)   │                          │
│ │               │ │               │                          │
│ │  HTML/CSS/JS  │ │  HTML/CSS/JS  │                          │
│ │  运行 Web 页面 │ │  运行 Web 页面 │                          │
│ └───────────────┘ └───────────────┘                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

进程职责对比

特性Main ProcessRenderer Process
数量1 个多个
环境Node.jsChromium
访问能力文件系统、系统 APIDOM、Web API
入口文件main.jsindex.html

IPC 通信

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

typescript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  sendMessage: (message) => ipcRenderer.send('message-channel', message)
});

// main.js
const { ipcMain } = require('electron');

ipcMain.on('message-channel', (event, message) => {
  console.log('Received:', message);
});

// renderer.js
window.electronAPI.sendMessage('Hello from renderer');

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

typescript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  invoke: (channel, data) => ipcRenderer.invoke(channel, data)
});

// main.js
ipcMain.handle('get-user-data', async (event, userId) => {
  const user = await database.getUser(userId);
  return user;
});

// renderer.js
const user = await window.electronAPI.invoke('get-user-data', 123);
console.log(user);

主进程 → 渲染进程

typescript
// main.js
const win = new BrowserWindow({ /* ... */ });

// 发送到特定窗口
win.webContents.send('update-available', { version: '2.0.0' });

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  onUpdateAvailable: (callback) => {
    ipcRenderer.on('update-available', (event, data) => callback(data));
  }
});

// renderer.js
window.electronAPI.onUpdateAvailable((data) => {
  console.log('New version:', data.version);
});

渲染进程 ↔ 渲染进程

typescript
// 通过主进程中转
// main.js
ipcMain.on('message-to-window', (event, { windowId, message }) => {
  const targetWindow = BrowserWindow.fromId(windowId);
  targetWindow?.webContents.send('message-from-window', message);
});

预加载脚本

为什么需要 Preload?

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  Main       │     │  Preload    │     │  Renderer   │
│  (Node.js)  │────▶│  (桥接层)   │────▶│  (Web)      │
│             │     │             │     │             │
│  有完整权限  │     │ 受限 Node   │     │  无 Node    │
└─────────────┘     └─────────────┘     └─────────────┘

安全的 Preload 实现

typescript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// ✅ 只暴露必要的 API
contextBridge.exposeInMainWorld('electronAPI', {
  // 文件操作
  readFile: (path) => ipcRenderer.invoke('fs:readFile', path),
  writeFile: (path, data) => ipcRenderer.invoke('fs:writeFile', path, data),
  
  // 系统信息
  getPlatform: () => process.platform,
  
  // 事件监听(带清理)
  onThemeChange: (callback) => {
    const handler = (event, theme) => callback(theme);
    ipcRenderer.on('theme-changed', handler);
    return () => ipcRenderer.removeListener('theme-changed', handler);
  }
});

// ❌ 危险:暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('ipc', ipcRenderer); // 不要这样做

主进程验证

typescript
// main.js
ipcMain.handle('fs:readFile', async (event, filePath) => {
  // 验证路径是否在允许范围内
  const allowedDir = app.getPath('userData');
  const resolvedPath = path.resolve(filePath);
  
  if (!resolvedPath.startsWith(allowedDir)) {
    throw new Error('Access denied');
  }
  
  return fs.readFileSync(resolvedPath, 'utf-8');
});

BrowserWindow

创建窗口

typescript
const { BrowserWindow, app } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    
    // 外观
    frame: true,                    // 是否显示边框
    titleBarStyle: 'hiddenInset',   // macOS 标题栏样式
    transparent: false,
    backgroundColor: '#ffffff',
    
    // 安全配置
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,       // 上下文隔离
      nodeIntegration: false,       // 禁用 Node
      sandbox: true                 // 沙箱模式
    }
  });

  // 开发环境加载 localhost
  if (process.env.NODE_ENV === 'development') {
    win.loadURL('http://localhost:3000');
    win.webContents.openDevTools();
  } else {
    win.loadFile('dist/index.html');
  }

  return win;
}

app.whenReady().then(createWindow);

窗口事件

typescript
win.on('close', (e) => {
  e.preventDefault();
  // 确认对话框
  dialog.showMessageBox(win, {
    type: 'question',
    buttons: ['Yes', 'No'],
    message: 'Are you sure you want to quit?'
  }).then(({ response }) => {
    if (response === 0) {
      win.destroy();
    }
  });
});

win.on('maximize', () => {
  win.webContents.send('window-maximized');
});

win.on('unmaximize', () => {
  win.webContents.send('window-unmaximized');
});

多窗口管理

typescript
const windows = new Map();

function createWindow(name, options = {}) {
  const win = new BrowserWindow({
    ...defaultOptions,
    ...options
  });
  
  windows.set(name, win);
  
  win.on('closed', () => {
    windows.delete(name);
  });
  
  return win;
}

function getWindow(name) {
  return windows.get(name);
}

应用生命周期

typescript
const { app } = require('electron');

// 所有窗口关闭时
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// macOS 点击 dock 图标
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

// 应用即将退出
app.on('before-quit', () => {
  // 保存状态等
});

// 阻止多实例
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (event, argv, workingDir) => {
    // 聚焦已有窗口
    const mainWindow = getWindow('main');
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
  });
}

高频面试题

Q1: 主进程和渲染进程的区别?

特性主进程渲染进程
入口main.jsHTML 文件
数量1 个多个
能力Node.js + Electron APIDOM + Web API
用途应用控制、系统交互UI 展示

Q2: 为什么需要 Context Isolation?

安全隔离:防止渲染进程中的恶意代码访问 Node.js API。

typescript
// 开启 contextIsolation 后
// preload 的变量不会泄露到 renderer
// 只有 contextBridge 暴露的 API 可用

Q3: IPC 通信有哪几种方式?

方式方向用途
ipcRenderer.send + ipcMain.onRenderer → Main单向通知
ipcRenderer.invoke + ipcMain.handleRenderer ↔ Main异步调用
webContents.sendMain → Renderer主动推送

Q4: 如何在渲染进程间通信?

  1. 通过主进程中转:推荐方式
  2. MessagePort:高性能场景
  3. 共享存储:localStorage/IndexedDB(同源限制)

项目结构

my-electron-app/
├── package.json
├── main/
│   ├── main.js           # 主进程入口
│   ├── preload.js        # 预加载脚本
│   └── ipc/
│       └── handlers.js   # IPC 处理器
├── renderer/
│   ├── index.html
│   ├── src/
│   │   ├── App.tsx
│   │   └── ...
│   └── public/
│       └── ...
└── resources/
    └── icons/

前端面试知识库