POST · 前端 · 文章
Electron 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 4011 字 · 预计阅读 11 分钟
本文整理一条可落地的 Electron 学习路径:从进程模型与最小项目起步,逐层深入 IPC 安全、工程化、原生能力、性能与体积优化、打包分发。
- 适用场景:Web 开发者转型桌面端、内部工具、跨平台客户端
- 技术栈:TypeScript / 现代前端框架(Vue / React 均可接入)
- 目标:应用能跑、能稳、能小、能发、能维护
一、先明确它在解决什么问题
Electron 不是"用网页糊一个桌面壳",它的价值在于:用一套前端技术栈同时交付三个平台的桌面客户端。
| 问题 | Electron 的答案 |
|---|---|
| 三端客户端要三套技术栈? | 一套 HTML/CSS/JS 全平台 |
| 前端团队做不了桌面? | 复用现有前端能力与生态 |
| Web 应用碰不到系统? | 主进程提供完整 Node.js 与系统 API |
| 迭代速度慢? | 前端热更新 + 独立发版节奏 |
代价必须提前说清楚:
| 代价 | 量级 | 是否可优化 |
|---|---|---|
| 安装包体积 | 通常 60~150MB | 可裁剪到 50MB 左右 |
| 内存占用 | 基础 100MB+,随窗口增长 | 可通过生命周期管理控制 |
| 冷启动 | 1~3 秒 | 可优化到 1 秒内 |
| 安全面 | 渲染层即 Web 攻击面 | 靠进程隔离与配置收敛 |
判断是否适合:需要跨平台、UI 复杂、团队是前端、对安装包体积不敏感——满足则合适;追求极致性能、极小体积、纯系统级工具——考虑 Tauri 或原生。
三个原则:
- 先分清进程职责,再写代码:主进程管系统,渲染进程管画面,preload 管边界。
- 安全配置不可妥协:
nodeIntegration、contextIsolation、sandbox三项是底线。 - 启动只做必要的事:主进程任何阻塞都会拖垮整个应用。
二、入门:环境与最小项目
2.1 环境准备
只需要 Node.js(推荐 LTS)与 npm。Electron 自带 Node 运行时,用户机器无需安装 Node。
node -v && npm -v
2.2 四个文件的最小骨架
不借助脚手架,手动建 4 个文件,先把结构看清楚。
package.json(main 指向主进程入口):
{
"name": "electron-demo",
"version": "1.0.0",
"main": "main.js",
"scripts": { "start": "electron ." },
"devDependencies": { "electron": "^latest" }
}
main.js(主进程,应用指挥官):
const { app, BrowserWindow } = require("electron");
const path = require("node:path");
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 600,
show: false, // 先不显示,避免白屏
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
});
win.once("ready-to-show", () => win.show());
win.loadFile("index.html");
}
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
preload.js(边界层,先留空):
// 后续在这里用 contextBridge 暴露受控 API
index.html(渲染进程,就是普通网页):
<!DOCTYPE html>
<html>
<body>
<h1>Hello Electron</h1>
<p id="ver"></p>
<script>
document.getElementById("ver").textContent = navigator.userAgent;
</script>
</body>
</html>
npm install && npm start
窗口出现即成功。此时在 DevTools 里输入 require 会报 undefined——这是设计,不是 bug。
三、核心:三进程模型
3.1 为什么必须隔离
渲染进程跑的是 Web 代码。一旦加载了第三方脚本,或把用户输入渲染成 HTML,出现 XSS 就意味着攻击者在你的页面里执行任意 JS。如果渲染进程还能 require('child_process'),一个 XSS 就等于整台机器沦陷。
所以规则只有一句:渲染进程负责画,主进程负责做,中间用 IPC 传话,preload 决定哪些话能传。
3.2 三个角色的职责
| 角色 | 运行环境 | 能做什么 | 不能做什么 |
|---|---|---|---|
| 主进程 | Node.js | 创建窗口、文件系统、托盘、菜单、全部系统 API | 不能跑长任务(会卡死所有窗口) |
| 渲染进程 | Chromium | 渲染 UI、处理交互 | 默认无 Node 权限,不能直接读文件 |
| Preload | 隔离上下文 | 用 contextBridge 暴露受控 API |
不能暴露完整的 ipcRenderer 或 require |
主进程全局只有一个,是所有窗口的调度中心;每个窗口对应一个独立渲染进程,内存互不共享。主进程一旦阻塞,整个应用(含所有窗口)都会无响应。
3.3 生命周期要点
| 事件 | 时机 | 典型用途 |
|---|---|---|
app.whenReady() |
主进程初始化完成 | 创建首个窗口 |
ready-to-show |
页面首帧可渲染 | 显示窗口,消除白屏 |
window-all-closed |
所有窗口关闭 | 非 macOS 退出应用 |
activate |
macOS 点击 Dock | 重建窗口 |
before-quit |
退出前 | 保存状态、清理资源 |
四、IPC 通信与安全边界
4.1 正确姿势:invoke / handle
旧方式(已废弃):remote 模块让渲染进程同步调用主进程对象,阻塞 + 漏洞,不要用。
当前最佳实践:contextBridge + ipcRenderer.invoke / ipcMain.handle,返回 Promise。
preload 只暴露具体方法:
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
getAppInfo: () => ipcRenderer.invoke("app:info"),
openFile: () => ipcRenderer.invoke("dialog:openFile"),
onProgress: callback => {
const handler = (_event, value) => callback(value);
ipcRenderer.on("download:progress", handler);
return () => ipcRenderer.removeListener("download:progress", handler);
}
});
主进程注册处理器:
const { ipcMain, dialog } = require("electron");
ipcMain.handle("app:info", () => ({
electron: process.versions.electron,
node: process.versions.node,
chromium: process.versions.chrome
}));
ipcMain.handle("dialog:openFile", async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"]
});
return canceled ? null : filePaths[0];
});
渲染进程调用:
const info = await window.api.getAppInfo();
const filePath = await window.api.openFile();
4.2 双向与推送
| 方向 | 方式 | 场景 |
|---|---|---|
| 渲染 → 主(请求响应) | invoke / handle |
读文件、弹对话框 |
| 渲染 → 主(单向) | send / on |
上报、触发动作 |
| 主 → 渲染 | webContents.send + preload on |
进度、菜单点击、更新提示 |
主进程推送时,preload 里的 on 必须返回取消订阅函数,组件卸载时调用,否则监听器泄漏。
4.3 安全边界的三条铁律
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
nodeIntegration: false:渲染进程拿不到 Node。contextIsolation: true:preload 与页面 JS 运行在隔离上下文,页面无法篡改 preload 暴露的对象。sandbox: true:渲染进程运行在沙箱内,进一步限制能力。
另外:主进程收到渲染进程传来的文件路径等参数必须校验,防止路径穿越;不要暴露 ipcRenderer 本身,否则等于绕过所有边界。
五、工程化:从手写到脚手架
手写 4 个文件适合理解原理,真实项目需要热更新与构建流程。推荐 electron-vite:
npm create @quick-start/electron@latest my-app -- --template vue
自动生成的标准结构:
my-app/
├─ src/
│ ├─ main/ 主进程(可多文件,按模块拆分)
│ ├─ preload/ 预加载脚本
│ └─ renderer/ Vue / React 页面
├─ electron.vite.config.ts
├─ electron-builder.yml
└─ package.json
收益:
| 能力 | 说明 |
|---|---|
| 渲染层 HMR | 改页面即时生效 |
| 主进程自动重启 | 改主进程代码自动重载 |
| 多入口构建 | main / preload / renderer 分别打包 |
| TypeScript 开箱 | 三端共享类型定义 |
主进程代码建议按职责拆目录:window/(窗口管理)、ipc/(处理器注册)、services/(文件、更新、托盘)、utils/。
六、原生能力实战
6.1 系统托盘
const { Tray, Menu, nativeImage } = require("electron");
const tray = new Tray(nativeImage.createFromPath("assets/tray.png"));
tray.setToolTip("My App");
tray.setContextMenu(
Menu.buildFromTemplate([
{ label: "显示", click: () => win.show() },
{ label: "退出", click: () => app.quit() }
])
);
tray.on("click", () => (win.isVisible() ? win.hide() : win.show()));
托盘图标各平台尺寸不同(macOS 用 Template Image,Windows 用 ico),要分别准备。
6.2 无边框窗口与自定义标题栏
new BrowserWindow({ frame: false });
CSS 中拖拽区与按钮区区分:
.titlebar { -webkit-app-region: drag; }
.titlebar button { -webkit-app-region: no-drag; }
适合播放器、工具类软件的沉浸式 UI。注意:拖拽区内的交互元素必须设为 no-drag,否则点不动。
6.3 菜单与快捷键
const { Menu, globalShortcut } = require("electron");
Menu.setApplicationMenu(
Menu.buildFromTemplate([
{ label: "文件", submenu: [{ role: "quit" }] },
{
label: "视图",
submenu: [{ role: "reload" }, { role: "toggleDevTools" }]
}
])
);
globalShortcut.register("CommandOrControl+Shift+K", () => win.show());
全局快捷键会占用系统按键,退出时记得 globalShortcut.unregisterAll()。
6.4 文件对话框与通知
const { dialog, Notification } = require("electron");
const result = await dialog.showSaveDialog({ defaultPath: "export.csv" });
new Notification({ title: "导出完成", body: result.filePath }).show();
Windows 通知需要设置 app.setAppUserModelId(),否则通知不显示或显示为 electron。
七、性能优化
7.1 消除白屏
窗口创建时 show: false,监听 ready-to-show 后再显示:
win.once("ready-to-show", () => win.show());
更极致的做法:先弹一个极轻量的 Loading 窗口,主窗口就绪后切换。
7.2 启动加速
| 手段 | 做法 |
|---|---|
| 延迟非核心模块 | 日志、统计、更新检测用 setTimeout 延后 |
| 关闭冗余特性 | app.commandLine.appendSwitch 关闭后台网络等 |
| 减少同步 IO | 启动路径上不做 readFileSync |
| 预加载拆分 | preload 只放必要 API,重逻辑放主进程异步 |
app.commandLine.appendSwitch("disable-background-networking");
7.3 渲染层性能
Electron 渲染层就是 Chromium,Web 性能优化手段全部适用:减少重排、虚拟列表、图片懒加载、requestIdleCallback 处理非紧急任务。区别在于不要在主进程做重活——CPU 密集任务应放到 utilityProcess 或 Worker。
7.4 窗口生命周期
窗口关闭时彻底销毁而非隐藏,避免内存常驻:
win.on("close", () => {
win.webContents.session.clearCache();
win = null;
});
渲染进程在 beforeunload 中清理定时器与 IPC 监听,避免闭包导致的内存泄漏。
八、体积与内存治理
8.1 体积控制
| 手段 | 效果 |
|---|---|
electron-builder 的 files 严格排除 |
去掉源码、sourcemap、测试资源 |
asar 打包 |
减少文件数、加快加载 |
| 依赖裁剪 | 用 electron-builder 的 npmRebuild 与依赖分析剔除无用包 |
| 压缩 | NSIS / dmg 自带压缩,Linux 用 AppImage |
# electron-builder.yml 片段
files:
- dist/**/*
- "!**/*.map"
- "!**/test/**"
asar: true
8.2 内存观测
| 指标 | 获取方式 |
|---|---|
| 主进程内存 | process.memoryUsage() |
| 各渲染进程 | app.getAppMetrics() |
| 泄漏排查 | DevTools Memory 面板 + 快照对比 |
app.getAppMetrics() 能拿到每个进程的 CPU 与内存,可接入自己的监控上报。
九、打包与分发
9.1 工具选择
| 工具 | 特点 | 适用 |
|---|---|---|
| electron-builder | 配置成熟、支持多平台与签名 | 主流选择 |
| electron-forge | 官方维护、插件化 | 新项目、喜欢官方生态 |
| electron-packager | 轻量、只打包不生成安装器 | 简单分发 |
9.2 平台产物
| 平台 | 产物 | 注意 |
|---|---|---|
| Windows | NSIS exe / MSI | 需代码签名,否则 SmartScreen 报警 |
| macOS | dmg / zip | 必须签名 + 公证(notarization),否则无法打开 |
| Linux | AppImage / deb | AppImage 免安装,deb 走包管理器 |
9.3 签名要点
- Windows:购买代码签名证书,配置
win.certificateFile与密码。 - macOS:Apple Developer 账号,配置
identity与notarize,CI 中走notarytool。 - 没有签名会导致:下载被拦截、更新失败、企业环境无法安装。
十、更新机制
10.1 方案
electron-updater 是最常用方案,配合 GitHub Releases 或私有更新服务器。
const { autoUpdater } = require("electron-updater");
autoUpdater.autoDownload = false;
autoUpdater.on("update-available", info => {
// 通过 IPC 通知渲染进程显示更新提示
});
autoUpdater.on("update-downloaded", () => {
autoUpdater.quitAndInstall();
});
autoUpdater.checkForUpdates();
10.2 要点
| 项 | 建议 |
|---|---|
| 更新提示 | 由渲染进程展示,主进程只做检查与安装 |
| 灰度 | 服务端按版本/渠道下发不同更新源 |
| 回滚 | 保留上一版本安装包,异常时可回退 |
| macOS | 必须签名 + 公证,否则更新失败 |
| 差量更新 | electron-updater 支持 blockmap 差量下载 |
十一、安全清单
生产环境逐项核对:
-
nodeIntegration: false、contextIsolation: true、sandbox: true - preload 只暴露具体方法,绝不暴露
ipcRenderer/require - 主进程对所有 IPC 参数做校验(路径、类型、范围)
- 不加载不可信远程内容;必须加载时用
<webview>并限制权限 - 永远不设
webSecurity: false - 禁用
remote模块 - 限制
shell.openExternal的 URL 白名单,防止命令注入 - 生产包关闭 DevTools 与调试端口
- 依赖定期审计(
npm audit),Electron 版本跟安全更新
十二、测试与调试
12.1 调试
| 对象 | 方式 |
|---|---|
| 渲染进程 | win.webContents.openDevTools() 或快捷键 |
| 主进程 | --inspect 参数 + Chrome DevTools 附加 |
| IPC | 主进程日志 + 渲染层调试面板 |
12.2 测试
| 层级 | 工具 | 内容 |
|---|---|---|
| 单元测试 | Vitest / Jest | 主进程 service、工具函数 |
| 端到端 | Playwright(支持 Electron) | 启动应用、点击、断言窗口 |
| 冒烟 | CI 中启动打包产物 | 验证能启动、关键流程可用 |
Playwright 的 Electron 支持可以真实启动应用并操作窗口,是目前 E2E 的主流选择。
十三、学习路径与阶段目标
| 阶段 | 时间 | 目标 | 验收 |
|---|---|---|---|
| 入门 | 第 1 周 | 跑通最小项目,理解三进程分工 | 用 invoke/handle 完成"点按钮→主进程读文件→显示内容" |
| 工程化 | 第 2 周 | 用 electron-vite 搭框架项目 | 接入托盘、菜单、通知,打出安装包 |
| 实战 | 第 3~4 周 | 做一个真实小工具 | Markdown 编辑器 / 剪贴板历史 / API 调试器 |
| 进阶 | 第 2 个月 | 性能与安全 | 冷启动 < 1s、安全清单全过、接入自动更新 |
| 深入 | 之后 | 原生模块、多窗口架构 | 按需学习,阅读官方 Performance / Security 指南 |
关键建议:先把进程模型与 IPC 安全打牢,窗口管理、原生能力、打包分发都是在同一地基上叠加。
十四、方案对比与选型
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| Electron | 生态成熟、前端零门槛、跨平台一致 | 体积大、内存高 | 复杂 UI、内部工具、跨平台客户端 |
| Tauri | 体积极小(几 MB)、内存低 | 需 Rust、WebView 各平台不一致 | 轻量工具、体积敏感 |
| 原生(Qt / Swift / WPF) | 性能与体积最优 | 三套技术栈、成本高 | 系统级、性能敏感 |
| Flutter Desktop | 一套代码多端、UI 一致 | 桌面生态较弱 | 已有 Flutter 团队 |
决策建议:
- 先看团队技术栈(前端团队优先 Electron)。
- 再看体积与性能敏感度(极敏感考虑 Tauri)。
- 最后看生态需求(需要大量 Node 能力选 Electron)。
十五、常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 启动白屏 | 窗口先显示、页面未就绪 | show: false + ready-to-show |
| 应用整体卡死 | 主进程跑了重任务 | 移到 utilityProcess / Worker |
渲染进程 require 报错 |
nodeIntegration 已关 |
用 preload + IPC |
| preload 暴露的 API 页面拿不到 | contextIsolation 下写法不对 |
用 contextBridge.exposeInMainWorld |
| 打包后白屏 | 资源路径用了绝对路径 | 用 loadFile 或配置 base |
| macOS 打不开应用 | 未签名 / 未公证 | 配置签名与 notarize |
| 通知不显示(Windows) | 未设 AppUserModelId | app.setAppUserModelId() |
| 托盘图标不显示 | 图标尺寸/格式不对 | 按平台准备 png/ico |
| 安装包过大 | 未排除开发文件 | 配置 files 与 asar |
| 更新失败 | 签名不一致或源配置错 | 检查证书与 publish 配置 |
十六、FAQ
Q1:Electron 和 Tauri 怎么选?
要生态、要 Node 能力、团队是前端,选 Electron;体积与内存极度敏感、团队能接受 Rust,选 Tauri。两者不是替代关系,是场景差异。
Q2:为什么安装包这么大?
每个 Electron 应用自带一份 Chromium 与 Node 运行时。可通过裁剪依赖、排除开发文件、asar 压缩控制在 50~80MB,但无法做到 Tauri 那样的几 MB。
Q3:渲染进程能直接用 Node 吗?
技术上能(打开 nodeIntegration),但生产环境不应这么做。任何 XSS 都会升级为系统级漏洞。
Q4:主进程能做耗时计算吗?
不能。主进程阻塞会让所有窗口无响应。CPU 密集任务放 utilityProcess、Worker 或子进程。
Q5:如何实现多窗口?
每个窗口 new BrowserWindow(),独立渲染进程。窗口间通信走主进程中转(webContents.send),不要依赖共享内存。
Q6:热更新怎么做?
业务代码(渲染层)可通过远程加载实现热更新,但主进程代码必须走安装包更新。多数产品选择整体走 electron-updater,保证版本一致。
Q7:如何减小内存占用?
关闭不用的窗口并销毁、限制同时打开的窗口数、避免渲染进程常驻定时器、用 app.getAppMetrics() 监控。
Q8:macOS 为什么必须公证?
Gatekeeper 会拦截未签名/未公证的应用,用户无法直接打开。公证是 Apple 的恶意软件扫描流程,CI 中用 notarytool 完成。
Q9:能做系统级能力吗(如读注册表、hook)?
可以,通过原生模块(Node-API)或调用系统命令。但会增加构建复杂度与平台适配成本。
Q10:Electron 应用如何做崩溃监控?
主进程监听 render-process-gone、child-process-gone,渲染层接入 Sentry 等错误监控,统一上报到自己的网关。