跳到主要内容

Electron 桌面开发

问题

Electron 的架构原理是什么?主进程与渲染进程如何通信?如何保证安全性、优化性能并进行打包分发?

答案

1. Electron 架构概述

Electron 是 GitHub 开源的跨平台桌面应用框架,基于 Chromium + Node.js + Native API 三层架构,允许使用 Web 技术(HTML、CSS、TypeScript)构建原生桌面应用。VS Code、Slack、Discord、Notion 等知名应用均基于 Electron 构建。

三层架构职责:

层级技术职责
渲染层Chromium页面渲染、CSS 布局、DOM 操作、DevTools
运行时层Node.js文件系统、网络请求、子进程、npm 生态
原生层Native API系统菜单、托盘、通知、对话框、全局快捷键
核心优势

Electron 的最大优势是 一套代码跨三端(Windows/macOS/Linux),且前端开发者无需学习 C++ 或 Swift/Objective-C 即可开发桌面应用。庞大的 npm 生态和 Chromium 的渲染能力使得 UI 开发几乎零门槛。

运行原理:

  1. 应用启动时,Electron 创建一个 主进程(Main Process)
  2. 主进程通过 BrowserWindow 创建一个或多个 渲染进程(Renderer Process)
  3. 每个渲染进程运行一个独立的 Chromium 网页实例
  4. 主进程与渲染进程通过 IPC(进程间通信) 交换数据
  5. 主进程可调用所有 Native API,渲染进程默认只能访问 Web API

2. 主进程与渲染进程

Electron 采用 多进程架构,与 Chromium 类似。理解主进程与渲染进程的区别是 Electron 开发的第一步。

2.1 主进程(Main Process)

主进程是应用的入口进程(通常是 main.ts),拥有完整的 Node.js 和 Electron API 访问权限:

main.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';

let mainWindow: BrowserWindow | null = null;

function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
sandbox: true,
},
});

mainWindow.loadFile('index.html');

mainWindow.on('closed', () => {
mainWindow = null;
});
}

// app 生命周期管理
app.whenReady().then(createWindow);

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

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

主进程职责:

  • 管理应用生命周期(app 模块)
  • 创建和管理窗口(BrowserWindow
  • 处理原生功能(菜单、托盘、对话框、通知)
  • 处理 IPC 消息
  • 管理自动更新

2.2 渲染进程(Renderer Process)

每个 BrowserWindow 对应一个独立的渲染进程,运行 Chromium 网页。默认情况下渲染进程 没有 Node.js API 访问权限(出于安全考虑)。

renderer.ts
// 渲染进程中只能访问 Web API 和通过 preload 暴露的接口
document.getElementById('btn')?.addEventListener('click', async () => {
// 通过 contextBridge 暴露的安全接口调用主进程功能
const result = await window.electronAPI.openFile();
console.log('选择的文件:', result);
});

2.3 进程对比

特性主进程渲染进程
数量有且仅有 1 个可以有多个
Node.js API完全可用默认不可用(需通过 preload 桥接)
Electron API全部可用仅部分可用(如 ipcRenderer
职责窗口管理、系统集成UI 渲染、用户交互
崩溃影响整个应用退出仅该窗口崩溃
入口package.jsonmain 字段BrowserWindow.loadFile/loadURL
进程隔离

渲染进程之间是完全隔离的,不能直接通信。两个渲染进程之间的通信必须通过主进程中转,或使用 MessagePort

3. IPC 通信机制

IPC(Inter-Process Communication)是 Electron 中主进程和渲染进程之间的通信桥梁。Electron 提供了多种 IPC 模式。

3.1 渲染进程 -> 主进程(单向)

preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (message: string) => ipcRenderer.send('message', message),
});
main.ts
import { ipcMain } from 'electron';

ipcMain.on('message', (_event, message: string) => {
console.log('收到消息:', message);
});

3.2 渲染进程 -> 主进程(双向,invoke/handle)

这是 推荐的双向通信模式,基于 Promise,类似 RPC 调用:

preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath),
});
main.ts
import { ipcMain, dialog } from 'electron';
import fs from 'fs/promises';

ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (canceled) return null;
return filePaths[0];
});

ipcMain.handle('fs:readFile', async (_event, filePath: string) => {
return fs.readFile(filePath, 'utf-8');
});
renderer.ts
const openBtn = document.getElementById('open-btn');
openBtn?.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile();
if (filePath) {
const content = await window.electronAPI.readFile(filePath);
console.log(content);
}
});

3.3 主进程 -> 渲染进程

主进程可以主动向指定窗口的渲染进程发送消息:

main.ts
// 主进程主动推送消息到渲染进程
mainWindow.webContents.send('update-available', { version: '2.0.0' });
preload.ts
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable: (callback: (data: { version: string }) => void) => {
ipcRenderer.on('update-available', (_event, data) => callback(data));
},
});

3.4 渲染进程之间通信(MessagePort)

Electron 7+ 支持使用 Web 标准的 MessagePort 实现渲染进程之间的直接通信:

main.ts
import { BrowserWindow, MessageChannelMain } from 'electron';

function connectRenderers(win1: BrowserWindow, win2: BrowserWindow): void {
const { port1, port2 } = new MessageChannelMain();
win1.webContents.postMessage('port', null, [port1]);
win2.webContents.postMessage('port', null, [port2]);
}

3.5 IPC 通信模式总结

模式方向API特点
send / on渲染 -> 主ipcRenderer.send + ipcMain.on单向,无返回值
invoke / handle渲染 ↔ 主ipcRenderer.invoke + ipcMain.handle推荐,Promise 双向
webContents.send主 → 渲染webContents.send + ipcRenderer.on主进程主动推送
MessagePort渲染 ↔ 渲染MessageChannelMain跨窗口直连
安全提示

绝对不要在 preload 中直接暴露整个 ipcRenderer 对象。应该只暴露具体的方法,并对参数进行校验,避免渲染进程可以任意调用主进程 IPC 通道。

4. Preload 脚本与安全模型

安全性是 Electron 应用开发的重中之重。由于渲染进程可能加载远程内容,不当的安全配置可能导致 远程代码执行(RCE) 漏洞。

4.1 contextIsolation(上下文隔离)

contextIsolation: true(Electron 12+ 默认启用)将 preload 脚本和网页的 JavaScript 上下文隔离,防止恶意网页篡改 preload 暴露的 API:

main.ts
new BrowserWindow({
webPreferences: {
contextIsolation: true, // 默认 true,隔离 preload 与网页上下文
sandbox: true, // 启用沙箱,限制 preload 中 Node.js API
nodeIntegration: false, // 默认 false,禁止渲染进程直接访问 Node.js
preload: path.join(__dirname, 'preload.js'),
},
});

4.2 contextBridge(安全桥接)

contextBridge 是安全地在隔离的上下文之间暴露 API 的唯一推荐方式:

preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
// 只暴露必要的方法,不暴露整个模块
getVersion: () => ipcRenderer.invoke('app:getVersion'),
minimize: () => ipcRenderer.send('window:minimize'),
onNotification: (callback: (msg: string) => void) => {
// 包装回调,避免暴露 event 对象
ipcRenderer.on('notification', (_event, msg) => callback(msg));
},
});
renderer.ts(类型声明)
// 为 window.electronAPI 添加类型声明
declare global {
interface Window {
electronAPI: {
getVersion: () => Promise<string>;
minimize: () => void;
onNotification: (callback: (msg: string) => void) => void;
};
}
}

4.3 安全最佳实践

main.ts — 安全配置
import { app, BrowserWindow, session } from 'electron';

function createSecureWindow(): BrowserWindow {
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
sandbox: true,
nodeIntegration: false,
webSecurity: true, // 启用同源策略
allowRunningInsecureContent: false, // 禁止 HTTPS 页面加载 HTTP 资源
preload: path.join(__dirname, 'preload.js'),
},
});

// 设置 CSP(Content Security Policy)
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
],
},
});
});

// 阻止新窗口打开(防止钓鱼)
win.webContents.setWindowOpenHandler(() => {
return { action: 'deny' };
});

return win;
}
危险配置

以下配置 绝不应该 在生产环境使用:

  • nodeIntegration: true — 让渲染进程直接访问 Node.js,极度危险
  • contextIsolation: false — 允许网页篡改 preload 暴露的 API
  • webSecurity: false — 禁用同源策略

如果渲染进程加载了恶意页面或被 XSS 攻击,上述配置会导致攻击者可以直接执行系统命令(如 require('child_process').exec('rm -rf /'))。

5. 窗口管理

5.1 BrowserWindow 创建与配置

main.ts — 窗口创建
import { BrowserWindow, screen } from 'electron';

function createWindow(): BrowserWindow {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;

const win = new BrowserWindow({
width: Math.min(1400, width),
height: Math.min(900, height),
minWidth: 800,
minHeight: 600,
title: 'My App',
icon: path.join(__dirname, 'assets/icon.png'),
show: false, // 先隐藏,渲染完毕再显示(避免白屏闪烁)
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
},
});

// 优雅显示:等 ready-to-show 后再展示窗口
win.once('ready-to-show', () => {
win.show();
});

return win;
}

5.2 多窗口管理

window-manager.ts
import { BrowserWindow } from 'electron';

class WindowManager {
private windows: Map<string, BrowserWindow> = new Map();

create(id: string, options: Electron.BrowserWindowConstructorOptions): BrowserWindow {
if (this.windows.has(id)) {
const existing = this.windows.get(id)!;
existing.focus();
return existing;
}

const win = new BrowserWindow(options);
this.windows.set(id, win);

win.on('closed', () => {
this.windows.delete(id);
});

return win;
}

get(id: string): BrowserWindow | undefined {
return this.windows.get(id);
}

closeAll(): void {
this.windows.forEach((win) => win.close());
}
}

export const windowManager = new WindowManager();

5.3 无边框窗口(Frameless Window)

frameless-window.ts
const win = new BrowserWindow({
frame: false, // 移除系统边框
titleBarStyle: 'hidden', // macOS: 隐藏标题栏但保留红绿灯按钮
titleBarOverlay: { // Windows: 使用系统覆盖按钮
color: '#1e1e1e',
symbolColor: '#ffffff',
height: 36,
},
transparent: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
},
});

要实现自定义标题栏的拖动功能,需在 CSS 中指定可拖动区域:

titlebar.css
.titlebar {
-webkit-app-region: drag; /* 可拖动 */
height: 36px;
display: flex;
align-items: center;
padding: 0 12px;
user-select: none;
}

.titlebar button {
-webkit-app-region: no-drag; /* 按钮区域不可拖动 */
}

6. 原生功能集成

6.1 系统菜单与上下文菜单

menu.ts
import { app, Menu, MenuItemConstructorOptions, BrowserWindow } from 'electron';

const template: MenuItemConstructorOptions[] = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: (_menuItem, browserWindow) => {
browserWindow?.webContents.send('menu:new-file');
},
},
{ type: 'separator' },
{ role: 'quit', label: '退出' },
],
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
],
},
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

6.2 系统托盘

tray.ts
import { Tray, Menu, nativeImage, app } from 'electron';

let tray: Tray | null = null;

function createTray(): void {
const icon = nativeImage.createFromPath(
path.join(__dirname, 'assets/tray-icon.png')
);
tray = new Tray(icon.resize({ width: 16, height: 16 }));

const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow?.show() },
{ label: '退出', click: () => app.quit() },
]);

tray.setToolTip('My App');
tray.setContextMenu(contextMenu);
tray.on('double-click', () => mainWindow?.show());
}

6.3 系统通知

notification.ts
import { Notification } from 'electron';

function showNotification(title: string, body: string): void {
const notification = new Notification({
title,
body,
icon: path.join(__dirname, 'assets/icon.png'),
silent: false,
});

notification.on('click', () => {
mainWindow?.show();
mainWindow?.focus();
});

notification.show();
}

6.4 全局快捷键

shortcuts.ts
import { globalShortcut, app } from 'electron';

app.whenReady().then(() => {
globalShortcut.register('CmdOrCtrl+Shift+Space', () => {
// 全局快捷键:即使应用在后台也能响应
mainWindow?.show();
mainWindow?.focus();
});
});

app.on('will-quit', () => {
globalShortcut.unregisterAll();
});

6.5 自动更新(electron-updater)

updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';
import log from 'electron-log';

export function setupAutoUpdater(mainWindow: BrowserWindow): void {
autoUpdater.logger = log;

autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update:available', info.version);
});

autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('update:progress', progress.percent);
});

autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('update:ready');
});

// 每 4 小时检查一次更新
setInterval(() => {
autoUpdater.checkForUpdates();
}, 4 * 60 * 60 * 1000);

autoUpdater.checkForUpdates();
}

// 渲染进程触发安装更新
export function installUpdate(): void {
autoUpdater.quitAndInstall(false, true); // 退出并安装
}

7. 打包与分发

7.1 electron-builder vs electron-forge

特性electron-builderelectron-forge
定位成熟的打包工具Electron 官方推荐的一体化工具链
输出格式dmg, nsis, AppImage, snap 等同样支持多平台
自动更新内置 electron-updater需配合 update.electronjs.org
构建速度较快中等
配置方式package.jsonelectron-builder.ymlforge.config.ts
Vite 集成需自行配置官方插件 @electron-forge/plugin-vite
生态社区广泛使用Electron 官方维护
推荐场景复杂打包需求、已有项目新项目、追求官方最佳实践

7.2 electron-builder 配置示例

electron-builder.yml
// package.json 中的 build 字段
{
"build": {
"appId": "com.example.myapp",
"productName": "My App",
"directories": {
"output": "release"
},
"mac": {
"target": ["dmg", "zip"],
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"gatekeeperAssess": false
},
"win": {
"target": ["nsis"],
"icon": "assets/icon.ico"
},
"linux": {
"target": ["AppImage", "deb"],
"category": "Development"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"publish": {
"provider": "github",
"owner": "your-org",
"repo": "your-app"
}
}
}

7.3 代码签名

代码签名是发布正式应用的必要步骤,否则 macOS 和 Windows 会警告用户:

平台签名机制费用
macOSApple Developer Certificate + Notarization$99/年
WindowsEV Code Signing Certificate(推荐)200200-500/年
LinuxGPG 签名(非强制)免费
macOS 公证配置(electron-builder)
{
"afterSign": "scripts/notarize.ts",
"mac": {
"hardenedRuntime": true,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
}
}

8. 性能优化

Electron 应用常被诟病的问题是 启动慢、内存占用高、包体积大。以下是针对性的优化策略。

8.1 冷启动优化

优化手段:

main.ts — 启动优化
// 1. 延迟加载非核心模块
const win = new BrowserWindow({
show: false, // 先隐藏
backgroundColor: '#1e1e1e', // 设置背景色避免白屏
});

// 2. ready-to-show 后再显示(避免白屏闪烁)
win.once('ready-to-show', () => {
win.show();
});

// 3. 主进程按需加载模块(减少启动时 require 的模块数量)
app.whenReady().then(async () => {
createWindow();
// 非核心功能延迟初始化
setTimeout(async () => {
const { setupAutoUpdater } = await import('./updater');
setupAutoUpdater(mainWindow!);
const { createTray } = await import('./tray');
createTray();
}, 3000);
});

关键优化点:

优化手段预期效果说明
show: false + ready-to-show消除白屏闪烁页面渲染完毕后再显示窗口
backgroundColor 设置减少感知延迟窗口创建时立即有背景色
主进程模块懒加载减少 200-500ms非核心模块延迟 import()
V8 代码缓存减少 JS 解析时间v8-compile-cache
预构建 native 模块避免安装时编译electron-rebuild

8.2 内存管理

每个渲染进程至少占用 30-80 MB 内存(Chromium 基础开销)。多窗口时内存占用会线性增长。

memory-optimization.ts
// 1. 隐藏窗口时释放渲染进程资源
win.on('hide', () => {
win.webContents.send('release-resources');
});

// 2. 关闭不需要的窗口(而非仅隐藏)
win.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
win.hide(); // macOS 下通常隐藏而非关闭
}
});

// 3. 使用 BrowserView 代替多个 BrowserWindow(共享进程)
import { BrowserView } from 'electron';

const view = new BrowserView();
win.addBrowserView(view);
view.setBounds({ x: 0, y: 0, width: 800, height: 600 });
view.webContents.loadURL('https://example.com');

8.3 减小包体积

Electron 应用打包后通常 150-300 MB,主要因为内嵌了完整的 Chromium 和 Node.js。

优化手段效果
使用 files 字段排除不必要的文件减少 10-50 MB
开启 asar 打包减少文件数,提升读取性能
使用 @electron/asarunpack 排除 native 模块避免 asar 解包
移除不需要的 node_modulesdevDependencies减少 20-100 MB
使用 Webpack/Vite 打包渲染进程代码减少 JS 体积
压缩资源(图片、字体)减少 5-20 MB
体积对比

如果应用对体积敏感,可以考虑 Tauri(基于系统 WebView,打包仅 2-10 MB)。但 Tauri 需要使用 Rust 编写后端逻辑,学习曲线较高。

9. Electron Forge 与现代工具链

Electron Forge 是 Electron 官方推荐的全流程工具链,集成了创建、开发、打包、发布等环节。

9.1 Vite 集成

forge.config.ts
import type { ForgeConfig } from '@electron-forge/shared-types';
import { VitePlugin } from '@electron-forge/plugin-vite';

const config: ForgeConfig = {
packagerConfig: {
asar: true,
icon: './assets/icon',
},
makers: [
{ name: '@electron-forge/maker-squirrel', config: {} },
{ name: '@electron-forge/maker-zip', platforms: ['darwin'] },
{ name: '@electron-forge/maker-deb', config: {} },
],
plugins: [
new VitePlugin({
build: [
{ entry: 'src/main.ts', config: 'vite.main.config.ts' },
{ entry: 'src/preload.ts', config: 'vite.preload.config.ts' },
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
}),
],
};

export default config;
vite.main.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
external: ['electron'],
},
},
});

9.2 项目结构

my-electron-app/
├── src/
│ ├── main.ts # 主进程入口
│ ├── preload.ts # preload 脚本
│ └── renderer/ # 渲染进程(React/Vue/Svelte)
│ ├── App.tsx
│ ├── index.html
│ └── main.tsx
├── forge.config.ts
├── vite.main.config.ts
├── vite.preload.config.ts
├── vite.renderer.config.ts
├── tsconfig.json
└── package.json

10. Electron vs Tauri 核心对比

特性ElectronTauri
渲染引擎内嵌 Chromium系统 WebView(WRY)
后端语言Node.js (JavaScript/TypeScript)Rust
打包体积150-300 MB2-10 MB
内存占用较高(200-500 MB)较低(50-150 MB)
启动速度较慢(2-5 秒)较快(0.5-2 秒)
跨平台一致性极高(自带 Chromium)中等(依赖系统 WebView)
Node.js 生态完全可用不可用(需 Rust 或 HTTP 桥接)
安全性需谨慎配置默认安全(Rust 内存安全)
社区生态成熟,插件丰富快速增长中
学习曲线低(前端开发者友好)中高(需学习 Rust)
适用场景功能复杂、需要 Node.js 生态的应用轻量工具、体积敏感的应用
代表应用VS Code、Slack、Discord暂无顶级应用
选型建议
  • 选 Electron:团队是纯前端技术栈、需要 Node.js 生态、追求跨平台 UI 一致性、功能复杂
  • 选 Tauri:对包体积和性能有极高要求、团队有 Rust 能力、应用相对轻量
  • 更多跨端方案对比请参阅 跨端方案对比与选型

常见面试问题

Q1: Electron 的主进程和渲染进程有什么区别?

答案

Electron 采用多进程架构,有且仅有一个 主进程(Main Process)和零到多个 渲染进程(Renderer Process)。

核心区别:

维度主进程渲染进程
数量有且仅有 1 个可以有多个(每个窗口一个)
入口package.jsonmain 字段BrowserWindow.loadFile() 加载的 HTML
Node.js完全可用默认禁用(出于安全考虑)
Electron API完整访问ipcRenderer 等少量 API
职责窗口管理、系统集成、IPC 中心UI 渲染、用户交互
崩溃影响整个应用崩溃仅该窗口崩溃

主进程负责创建和管理 BrowserWindow、调用原生 API(菜单、托盘、通知等),渲染进程只负责页面展示和用户交互。两者通过 IPC 通信。

Q2: 如何在主进程和渲染进程之间通信?

答案

Electron 提供了多种 IPC 通信方式:

1. invoke/handle(推荐,双向 Promise):

// preload.ts
contextBridge.exposeInMainWorld('api', {
readFile: (path: string) => ipcRenderer.invoke('fs:read', path),
});

// main.ts
ipcMain.handle('fs:read', async (_e, path: string) => {
return fs.readFile(path, 'utf-8');
});

// renderer.ts
const content = await window.api.readFile('/path/to/file');

2. send/on(渲染 -> 主,单向):

ipcRenderer.send('log', 'something happened');
ipcMain.on('log', (_e, msg) => console.log(msg));

3. webContents.send(主 -> 渲染):

mainWindow.webContents.send('notification', '更新可用');

4. MessagePort(渲染 ↔ 渲染直连):

const { port1, port2 } = new MessageChannelMain();
win1.webContents.postMessage('port', null, [port1]);
win2.webContents.postMessage('port', null, [port2]);

推荐使用 invoke/handle 模式,它基于 Promise、支持返回值、语义清晰。

Q3: contextBridge 和 preload 的作用是什么?

答案

preload 脚本 在渲染进程的网页加载之前执行,拥有部分 Node.js 和 Electron API 的访问权限。它是主进程与渲染进程之间的"安全桥梁"。

contextBridge 是在开启 contextIsolation: true 时,唯一安全地向渲染进程暴露 API 的方式。它将 preload 中定义的对象安全地注入到网页的 window 对象中。

// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
openDialog: () => ipcRenderer.invoke('dialog:open'),
});
// 渲染进程通过 window.electronAPI.openDialog() 调用

为什么需要 contextBridge 而不是直接挂 window?

如果不用 contextBridge,在 contextIsolation: true 下,preload 和网页运行在不同的 JavaScript 上下文中,直接给 window 赋值对网页不可见。contextBridge 通过安全的克隆机制跨上下文传递数据,且网页无法篡改 preload 中的原始函数。

Q4: Electron 应用如何保证安全性?

答案

Electron 安全的核心原则是 最小权限,关键措施包括:

  1. 开启 contextIsolation(默认 true):隔离 preload 和网页上下文
  2. 禁用 nodeIntegration(默认 false):渲染进程无法直接访问 Node.js
  3. 启用 sandbox(推荐 true):进一步限制 preload 中可用的 Node.js API
  4. 使用 contextBridge 最小暴露 API:只暴露必要的方法,不传递整个模块
  5. 设置 CSP:通过 Content-Security-Policy 限制脚本和资源加载源
  6. 验证 IPC 输入:主进程中对 IPC 参数进行类型和范围校验
  7. 阻止新窗口导航setWindowOpenHandler 返回 { action: 'deny' }
  8. 限制 webview 标签:如果不需要 <webview>,应该禁用
  9. 保持 Electron 版本更新:及时修复 Chromium 安全漏洞

Q5: 如何优化 Electron 应用的启动速度?

答案

Electron 启动主要包括:主进程初始化 -> 创建窗口 -> 加载 HTML -> 执行 JS -> 渲染。优化策略:

阶段优化手段
主进程延迟加载非核心模块(动态 import()),减少初始 require
窗口创建show: false + ready-to-show 显示,设置 backgroundColor
HTML 加载使用本地文件而非远程 URL,内联关键 CSS
JS 执行代码分割、Tree Shaking、启用 V8 代码缓存
渲染骨架屏过渡、延迟渲染非首屏内容
// 延迟加载示例
app.whenReady().then(() => {
createWindow(); // 先创建窗口
// 3 秒后再初始化非核心功能
setTimeout(async () => {
const { setupTray } = await import('./tray');
setupTray();
}, 3000);
});

Q6: 如何实现 Electron 应用的自动更新?

答案

最常用的方案是 electron-updater(配合 electron-builder),支持全量更新和差量更新(delta update)。

基本流程:

  1. 构建时生成更新文件(latest.yml + 安装包),发布到 GitHub Releases/S3 等
  2. 应用启动后调用 autoUpdater.checkForUpdates() 检查更新
  3. 发现新版本后后台下载
  4. 下载完成后提示用户重启安装,或调用 autoUpdater.quitAndInstall()
import { autoUpdater } from 'electron-updater';

autoUpdater.on('update-available', () => {
// 通知渲染进程有更新
mainWindow.webContents.send('update:available');
});

autoUpdater.on('update-downloaded', () => {
// 用户确认后安装
autoUpdater.quitAndInstall(false, true);
});

autoUpdater.checkForUpdates();

macOS 需要代码签名 + 公证才能使用自动更新;Windows 使用 NSIS 安装器时支持静默更新。

Q7: Electron 打包体积大怎么办?

答案

Electron 内嵌完整 Chromium + Node.js,基础体积 ~150 MB,优化思路:

  1. 排除不必要文件:在 package.jsonbuild.files 中精确指定需要打包的文件
  2. 移除 devDependencies:确保 devDependencies 不被打包进去
  3. 使用 Vite/Webpack 打包渲染进程代码:Tree Shaking + 代码压缩
  4. 启用 asar:将应用代码打包为 asar 归档,减少文件数量
  5. 压缩资源:图片使用 WebP、字体使用 woff2 子集
  6. 使用 electron-builder--compression 选项:打包时开启最大压缩

如果体积是核心诉求(如分发给用户下载),可以考虑切换到 Tauri(2-10 MB),但需要 Rust 能力。

Q8: Electron 中如何处理崩溃和错误?

答案

// 1. 主进程未捕获异常
process.on('uncaughtException', (error) => {
log.error('主进程未捕获异常:', error);
// 可以弹出错误对话框
dialog.showErrorBox('错误', error.message);
});

// 2. 渲染进程崩溃检测
mainWindow.webContents.on('render-process-gone', (event, details) => {
log.error('渲染进程崩溃:', details.reason);
if (details.reason === 'crashed') {
// 重新加载或提示用户
mainWindow.reload();
}
});

// 3. 使用 crashReporter 上报崩溃
import { crashReporter } from 'electron';
crashReporter.start({
submitURL: 'https://your-crash-server.com/report',
productName: 'MyApp',
companyName: 'MyCompany',
});

// 4. 渲染进程中使用 window.onerror 和 unhandledrejection
window.addEventListener('unhandledrejection', (event) => {
window.electronAPI.reportError(event.reason);
});

Q9: Electron 如何实现多窗口通信?

答案

渲染进程之间不能直接通信,有三种方案:

方案一:通过主进程中转(最常用)

// 渲染进程 A 发送
window.electronAPI.sendToOther('data');
// 主进程转发
ipcMain.on('send-to-other', (event, data) => {
otherWindow.webContents.send('from-other', data);
});

方案二:MessagePort 直连(高频通信推荐)

const { port1, port2 } = new MessageChannelMain();
winA.webContents.postMessage('port', null, [port1]);
winB.webContents.postMessage('port', null, [port2]);
// 建立连接后两个渲染进程可以直接通信,无需经过主进程

方案三:SharedWorker(Web 标准)

如果两个窗口加载的是同一个源(origin),可以使用 SharedWorker 实现通信。

Q10: 什么是 Electron 的沙箱模式?

答案

沙箱模式(sandbox: true)启用 Chromium 的 OS 级沙箱机制,进一步限制渲染进程和 preload 脚本的能力:

  • 未启用沙箱:preload 脚本可以使用部分 Node.js API(如 requireprocessBuffer
  • 启用沙箱:preload 脚本只能使用 contextBridgeipcRenderer、少数 Electron API,无法直接 require Node.js 模块
new BrowserWindow({
webPreferences: {
sandbox: true, // Electron 20+ 默认启用
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});

沙箱模式通过 OS 级别的进程隔离(Linux 的 seccomp-bpf、macOS 的 App Sandbox、Windows 的 restricted token)限制进程能做的系统调用,即使攻击者在渲染进程中执行了任意 JS,也无法逃逸到操作系统层面。

Q11: Electron 如何实现无边框窗口和自定义标题栏?

答案

// 创建无边框窗口
const win = new BrowserWindow({
frame: false, // 完全移除系统边框
// 或 macOS 专用:保留红绿灯按钮
titleBarStyle: 'hidden',
trafficLightPosition: { x: 12, y: 12 },
// Windows 专用:系统覆盖按钮
titleBarOverlay: {
color: '#2f3241',
symbolColor: '#fff',
height: 32,
},
});

CSS 中需要设置拖动区域:

/* 自定义标题栏区域可拖动 */
.titlebar { -webkit-app-region: drag; }
/* 按钮等交互元素不可拖动 */
.titlebar button { -webkit-app-region: no-drag; }

注意:无边框窗口需要自行实现最小化、最大化、关闭按钮,通过 IPC 调用 win.minimize()win.maximize()win.close()

Q12: Electron Forge 和 electron-builder 该怎么选?

答案

场景推荐
新项目,想用 Vite + TSElectron Forge(官方 Vite 插件,开箱即用)
已有项目迁移到 Electronelectron-builder(配置灵活,侵入性低)
需要复杂的自动更新(差量更新、多通道)electron-builder(electron-updater 更成熟)
追求 Electron 官方最佳实践Electron Forge(Electron 团队维护)
需要发布到多平台商店electron-builder(更多 target 格式支持)

Electron Forge 是 Electron 官方推荐的工具链,集成了 create/package/make/publish 完整流程。新项目建议优先考虑 Forge。electron-builder 则在社区有更广泛的使用基础和更多的配置选项。

Q13: Electron 和 Tauri 怎么选?

答案

选 Electron 的场景:

  • 团队纯前端技术栈,不熟悉 Rust
  • 需要 Node.js 生态(如使用 native addon、npm 包)
  • 追求跨平台 UI 完全一致(内嵌 Chromium)
  • 应用功能复杂,如 VS Code 级别

选 Tauri 的场景:

  • 对包体积和内存占用有严格要求(Tauri 打包仅 2-10 MB)
  • 团队有 Rust 能力或愿意学习
  • 应用相对轻量,如工具类应用
  • 安全性要求极高(Rust 内存安全 + 默认最小权限)

核心权衡:Electron = 生态成熟 + 一致性 + 大体积Tauri = 轻量 + 高性能 + Rust 门槛

更多跨端方案的完整对比请参阅 跨端方案对比与选型

Q14: 如何调试 Electron 应用?

答案

渲染进程调试:

  • 使用 Chrome DevTools(win.webContents.openDevTools()
  • 支持 Elements、Console、Network、Performance 等所有面板
  • React/Vue DevTools 扩展可通过 electron-devtools-installer 安装

主进程调试:

// 1. VS Code 调试配置
// .vscode/launch.json
{
"type": "node",
"request": "launch",
"name": "Debug Main Process",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"args": ["."],
"sourceMaps": true
}

// 2. 命令行启动
// electron --inspect=5858 .

其他调试工具:

  • electron-devtools-installer:安装 React/Vue DevTools
  • electron-log:跨进程日志记录
  • Chromium chrome://tracing:性能分析
  • process.crash() + crashReporter:崩溃调试

Q15: Electron 中如何处理 Node.js native 模块?

答案

Electron 内置的 Node.js 版本可能与系统安装的不同,native 模块(如 better-sqlite3node-canvas)需要针对 Electron 的 Node.js 版本重新编译。

# 使用 electron-rebuild 重新编译 native 模块
npx electron-rebuild

# 或在 package.json 中配置 postinstall 钩子
{
"scripts": {
"postinstall": "electron-rebuild"
}
}

使用 Electron Forge 时,Vite 插件会自动将 native 模块标记为 external,无需手动配置。

避免 native 模块

如果可能,优先使用纯 JS 替代方案(如用 sql.js 替代 better-sqlite3),可以避免 native 模块带来的编译和兼容性问题。

相关链接