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 或原生。

三个原则:

  1. 先分清进程职责,再写代码:主进程管系统,渲染进程管画面,preload 管边界。
  2. 安全配置不可妥协:nodeIntegration、contextIsolation、sandbox 三项是底线。
  3. 启动只做必要的事:主进程任何阻塞都会拖垮整个应用。

二、入门:环境与最小项目

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 团队

决策建议:

  1. 先看团队技术栈(前端团队优先 Electron)。
  2. 再看体积与性能敏感度(极敏感考虑 Tauri)。
  3. 最后看生态需求(需要大量 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 等错误监控,统一上报到自己的网关。

← 返回帖子 更多前端内容 →