Electron 架构

主进程、渲染进程、预加载脚本、IPC 的四层结构与职责边界。

Beginner
Key Points主进程管窗口与系统,渲染进程管界面,preload 是两者之间唯一的桥。

三层结构

Text
┌─────────────────────────────────────────────┐
│ Main process (Node.js)                      │
│  app / BrowserWindow / Menu / Tray / dialog │
│  读写文件、调命令行、管生命周期                │
└───────────────┬─────────────────────────────┘
                │ IPC (ipcMain / ipcRenderer)
┌───────────────┴─────────────────────────────┐
│ Preload script(桥)                         │
│  contextBridge.exposeInMainWorld(...)       │
└───────────────┬─────────────────────────────┘
                │ window.api.*
┌───────────────┴─────────────────────────────┐
│ Renderer process (Chromium)                 │
│  HTML / CSS / 前端框架,一个窗口一个进程       │
└─────────────────────────────────────────────┘
ProcessRunsCan access
MainNode.js全部系统 API、所有窗口
PreloadNode + DOM(受限)两者,但只暴露白名单
RendererChromium只有 DOM,除非通过桥

主进程

JavaScript
const { app, BrowserWindow, dialog } = require('electron');
const path = require('node:path');

let win;

function createWindow() {
  win = new BrowserWindow({
    width: 1000,
    height: 700,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,     // 默认值,别关
      nodeIntegration: false      // 默认值,别开
    }
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

There is exactly one main process. It renders nothing; it creates windows, registers menus and reacts to system events. A click in the UI that needs a system capability must message it.

预加载脚本

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

contextBridge.exposeInMainWorld('api', {
  readFile: (p) => ipcRenderer.invoke('file:read', p),
  saveFile: (p, text) => ipcRenderer.invoke('file:write', p, text),
  onProgress: (cb) => ipcRenderer.on('progress', (_e, v) => cb(v))
});

exposeInMainWorld is the only recommended way to expose anything. It puts a set of functions on window.api; the page can call them but cannot reach require, process or ipcRenderer itself.

IPC 两种模式

PatternMain sideRenderer sideUse
invoke / handleipcMain.handleipcRenderer.invoke要返回值,最常用
send / onipcMain.onipcRenderer.send发完不管结果
webContents.sendwin.webContents.sendipcRenderer.on进度、推送
JavaScript
// main.js —— 注册处理器
const { ipcMain } = require('electron');
const fs = require('node:fs/promises');

ipcMain.handle('file:read', async (_event, filePath) => {
  try {
    return { ok: true, data: await fs.readFile(filePath, 'utf8') };
  } catch (e) {
    return { ok: false, error: e.message };
  }
});
JavaScript
// 页面里调用
const res = await window.api.readFile('/tmp/notes.txt');
if (res.ok) {
  document.getElementById('out').textContent = res.data;
} else {
  console.error(res.error);
}
An exception thrown in ipcMain.handle reaches the renderer as a rejected promise wrapped as Error invoking remote method, losing the original stack. Catch inside the handler and return {ok, error} instead.

生命周期

JavaScript
app.whenReady().then(() => {
  createWindow();

  // macOS:点 Dock 图标且没有窗口时重建一个
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 除 macOS 外,关掉所有窗口就退出
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

macOS convention keeps the app alive when windows close; Windows and Linux quit. These three blocks are boilerplate in every Electron project, and omitting activate makes the Dock icon do nothing on macOS.

Friends & Partners