Electron 架构
主进程、渲染进程、预加载脚本、IPC 的四层结构与职责边界。
三层结构
┌─────────────────────────────────────────────┐
│ Main process (Node.js) │
│ app / BrowserWindow / Menu / Tray / dialog │
│ 读写文件、调命令行、管生命周期 │
└───────────────┬─────────────────────────────┘
│ IPC (ipcMain / ipcRenderer)
┌───────────────┴─────────────────────────────┐
│ Preload script(桥) │
│ contextBridge.exposeInMainWorld(...) │
└───────────────┬─────────────────────────────┘
│ window.api.*
┌───────────────┴─────────────────────────────┐
│ Renderer process (Chromium) │
│ HTML / CSS / 前端框架,一个窗口一个进程 │
└─────────────────────────────────────────────┘| Process | Runs | Can access |
|---|---|---|
| Main | Node.js | 全部系统 API、所有窗口 |
| Preload | Node + DOM(受限) | 两者,但只暴露白名单 |
| Renderer | Chromium | 只有 DOM,除非通过桥 |
主进程
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.
预加载脚本
// 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 两种模式
| Pattern | Main side | Renderer side | Use |
|---|---|---|---|
| invoke / handle | ipcMain.handle | ipcRenderer.invoke | 要返回值,最常用 |
| send / on | ipcMain.on | ipcRenderer.send | 发完不管结果 |
| webContents.send | win.webContents.send | ipcRenderer.on | 进度、推送 |
// 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 };
}
});// 页面里调用
const res = await window.api.readFile('/tmp/notes.txt');
if (res.ok) {
document.getElementById('out').textContent = res.data;
} else {
console.error(res.error);
}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.生命周期
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.
三层结构
┌─────────────────────────────────────────────┐
│ Main process (Node.js) │
│ app / BrowserWindow / Menu / Tray / dialog │
│ 读写文件、调命令行、管生命周期 │
└───────────────┬─────────────────────────────┘
│ IPC (ipcMain / ipcRenderer)
┌───────────────┴─────────────────────────────┐
│ Preload script(桥) │
│ contextBridge.exposeInMainWorld(...) │
└───────────────┬─────────────────────────────┘
│ window.api.*
┌───────────────┴─────────────────────────────┐
│ Renderer process (Chromium) │
│ HTML / CSS / 前端框架,一个窗口一个进程 │
└─────────────────────────────────────────────┘| 进程 | 运行内容 | 能访问 |
|---|---|---|
| 主进程 | Node.js | 全部系统 API、所有窗口 |
| 预加载 | Node + DOM(受限) | 两者,但只暴露白名单 |
| 渲染 | Chromium | 只有 DOM,除非通过桥 |
主进程
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();
});整个应用只有一个主进程。它不做界面,只负责「创建窗口、注册菜单、响应系统事件」。界面里的点击要调系统能力,必须发消息给它。
预加载脚本
// 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 是唯一推荐的暴露方式。它把一组函数挂到 window.api 上,页面能调,但拿不到 require、process、ipcRenderer 本身。
IPC 两种模式
| 模式 | 主进程侧 | 渲染侧 | 适用 |
|---|---|---|---|
| 请求响应 | ipcMain.handle | ipcRenderer.invoke | 要返回值,最常用 |
| 单向通知 | ipcMain.on | ipcRenderer.send | 发完不管结果 |
| 主推子 | win.webContents.send | ipcRenderer.on | 进度、推送 |
// 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 };
}
});// 页面里调用
const res = await window.api.readFile('/tmp/notes.txt');
if (res.ok) {
document.getElementById('out').textContent = res.data;
} else {
console.error(res.error);
}ipcMain.handle 抛出的异常在渲染侧会变成一个被拒绝的 Promise,且错误信息会被包装成 Error invoking remote method——原始堆栈看不到。所以处理器里最好 try/catch 后返回 {ok, error} 结构。生命周期
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 的惯例是「关窗口不退出应用」,Windows 和 Linux 相反。这三段代码几乎是每个 Electron 项目的固定模板,漏写 activate 会导致 mac 上点 Dock 图标没反应。