一份完整的 Electron 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),覆盖从入门到打包发布的全链路知识。以 VS Code、Slack 等真实应用为案例。
📌 打个比方:Electron 像给你一套精装房——自带全套家电(Chromium 浏览器引擎 + Node.js 运行时),拎包入住,装修风格统一。代价是房子比较重(安装包 80+ MB),但省去了"不同系统家具风格不一致"的烦恼。这就是 Electron 和 Tauri 的核心区别:Electron 捆绑一切,Tauri 复用系统 WebView。
| 应用 | 用户量 | 说明 |
|---|---|---|
| VS Code | 数千万 | 微软的代码编辑器,Electron 最成功的案例 |
| Slack | 数千万 | 团队协作工具 |
| Discord | 上亿 | 游戏社区语音/文字聊天 |
| Figma Desktop | 数百万 | 设计工具桌面版 |
| Notion | 数千万 | 笔记/知识管理 |
| 1Password | 数百万 | 密码管理器 |
| GitHub Desktop | 数百万 | Git 图形客户端 |
📌 为什么大厂爱用 Electron?因为前端团队直接就能写桌面应用,不需要学 C++/Qt/Rust。一套 Web 技术栈 → Windows、macOS、Linux 三端通吃。对初创团队来说,"能快速出产品"比"安装包小 50MB"重要得多。
| 层 | 作用 | 技术栈 |
|---|---|---|
| 主进程 (Main) | 管理应用生命周期、窗口、原生 API | Node.js + Electron Main API |
| 渲染进程 (Renderer) | 渲染 UI、用户交互 | Chromium + 前端框架(React/Vue/Svelte…) |
| IPC 通信层 | 主进程与渲染进程间消息传递 | ipcMain / ipcRenderer / contextBridge |
| 原生 API 桥接 | 将系统能力暴露给 JS | C++ 底层模块 |
📌 为什么有两个进程?Chromium 本身就是多进程架构——每个网页标签页是独立的渲染进程。Electron 继承了这个设计:主进程管"壳"(窗口、菜单、系统交互),渲染进程管"内容"(UI、用户交互)。这和浏览器的"浏览器进程 + 标签页进程"是一一对应的。Node.js 能力只在主进程完整可用,渲染进程默认被沙箱隔离。
| 维度 | Electron | Tauri |
|---|---|---|
| 渲染引擎 | 捆绑 Chromium | 系统自带 WebView |
| 后端运行时 | 捆绑 Node.js | Rust |
| 安装包大小 | ~80-150 MB | ~3-10 MB |
| 内存占用 | 较高(Chromium 开销) | 较低 |
| API 一致性 | 所有平台渲染一致 | 不同系统 WebView 有差异 |
| 学习曲线 | 低(前端开发者直接上手) | 中(需要学 Rust 基础) |
| 生态成熟度 | 非常成熟(2013 至今) | 较新(2020+) |
| 原生模块 | N-API / node-native-addon | Rust 插件 |
📌 选型建议:如果你的团队全是前端开发者,需要快速出产品,不在乎包体积——选 Electron。如果你追求极致轻量、安全,或者已经会用 Rust——选 Tauri。两者不是互斥的,很多概念(主进程/渲染进程、IPC 通信)是相通的。
📌 为什么推荐 electron-forge?因为它集成了打包、自动更新、代码签名等全流程工具,不需要你再手动拼凑 electron-builder + electron-updater + squirrel。Forge 是 Electron 官方推荐的"一站式"工具链。
🚧 main 字段是 Electron 的入口点。Electron 启动时首先执行 main 指向的文件(主进程)。如果忘了配,Electron 会默认找 index.js,找不到就报错。
📌 preload 脚本是安全桥梁。它在渲染进程的页面加载之前执行,有受限的 Node.js 访问权。通过 contextBridge 暴露安全的 API 给渲染进程,避免渲染进程直接接触 Node.js——这是 Electron 安全模型的核心。
主进程是 Electron 的"大脑",负责:
app.whenReady()、app.on('window-all-closed')、app.quit()BrowserWindowMenu)、托盘 (Tray)、对话框 (dialog)fs、path、http、child_process 等所有 Node 模块渲染进程就是"网页",负责 UI 渲染和用户交互:
document、window、fetch、Canvas 等nodeIntegration: false📌 为什么不能直接在渲染进程用 Node.js?因为渲染进程加载的网页可能包含第三方内容(npm 包、CDN 脚本),如果开启了 nodeIntegration,恶意脚本就能通过 require('child_process') 执行系统命令——这是 2018 年前 Electron 应用的常见安全漏洞。contextIsolation + preload + contextBridge 是 Electron 官方的安全最佳实践。
📌 VS Code 就是用无边框窗口 + 自定义标题栏。这样可以在标题栏放命令面板、搜索框等自定义 UI。代价是你得自己实现拖拽区域(-webkit-app-region: drag)和窗口控制按钮(最小化/最大化/关闭)。
异步请求-响应模式,类似 HTTP 请求:
渲染进程 → 主进程的单向消息:
| 模式 | 主进程 | 渲染进程 | 场景 |
|---|---|---|---|
| 请求-响应 | ipcMain.handle() |
ipcRenderer.invoke() |
读文件、调系统 API |
| 单向通知 R→M | ipcMain.on() |
ipcRenderer.send() |
最小化、退出等操作 |
| 单向通知 M→R | webContents.send() |
ipcRenderer.on() |
推送更新、状态变化 |
| 双向通信 | ipcMain.on() + event.reply() |
ipcRenderer.send() + ipcRenderer.on() |
长连接式通信 |
📌 为什么用 invoke/handle 而不是 send/on?因为 invoke 返回 Promise,天然支持 async/await,错误也能通过 Promise rejection 传递。send/on 是事件驱动的,需要手动管理回调,容易遗漏错误处理。新代码一律用 invoke/handle。
📌 macOS 的菜单栏在屏幕顶部,不是在窗口上。所以即使没有打开窗口,菜单栏也要存在。Electron 通过 app.dock 和菜单的 role: 'appMenu' 处理这个差异。第一个菜单项在 macOS 上会变成应用名菜单(包含"关于"、"退出"等)。
🚧 托盘图标要清理。应用退出时如果不销毁 Tray 实例,托盘图标会残留在系统托盘区(Windows 上尤其明显)。记得在 app.on('before-quit') 中 tray.destroy()。
📌 为什么文件操作在主进程做?因为渲染进程默认没有 Node.js 的 fs 模块(nodeIntegration: false)。文件操作通过 IPC 转发给主进程执行,主进程有完整的 Node.js 能力。这既是安全隔离,也是职责分离——渲染进程管 UI,主进程管系统操作。
🚧 全局快捷键要谨慎。它在你应用没聚焦时也生效,可能和系统快捷键或其他应用冲突。注册前检查返回值:const ret = globalShortcut.register(...); if (!ret) console.log('注册失败')。
📌 为什么用 better-sqlite3 而不是 sqlite3?better-sqlite3 是同步 API,代码更简洁,性能更好(C++ 绑定,不需要 Promise 开销)。在主进程中用同步数据库操作完全没问题——主进程不是 UI 线程,不会卡 UI。
📌 Electron 性能优化的核心思路:Electron 本质是 Chromium + Node.js,所以 Web 性能优化技巧全部适用(虚拟滚动、代码分割、懒加载等)。额外注意的是 IPC 通信开销——每次 invoke 都是跨进程序列化/反序列化,高频调用会成为瓶颈。批量传递数据,避免在循环中调用 IPC。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
nodeIntegration |
false |
渲染进程禁用 Node.js |
contextIsolation |
true |
隔离 preload 和渲染进程上下文 |
sandbox |
true |
启用 Chromium 沙箱 |
webSecurity |
true |
启用同源策略 |
allowRunningInsecureContent |
false |
禁止加载 HTTP 资源 |
preload |
指定 preload 脚本 | 通过 contextBridge 暴露安全 API |
🚧 永远不要用 ipcRenderer.on 直接暴露给网页。如果渲染进程加载了第三方内容(比如广告、用户生成内容),恶意脚本可以监听你的 IPC 通道。始终通过 contextBridge 精确暴露需要的 API。
| 平台 | 格式 | 说明 |
|---|---|---|
| Windows | .exe (NSIS/Squirrel) |
最常用,NSIS 支持自定义安装界面 |
| Windows | .msi (WiX) |
企业部署友好 |
| Windows | .appx |
Microsoft Store |
| macOS | .dmg |
磁盘镜像,拖拽安装 |
| macOS | .pkg |
安装器 |
| Linux | .AppImage |
免安装,单文件运行 |
| Linux | .deb |
Debian/Ubuntu |
| Linux | .rpm |
Fedora/RHEL |
| Linux | .snap |
Snap Store |
📌 为什么 asar 不加密?Electron 的哲学是"开源友好"。asar 的目的是减少文件数量(从几百个文件变成一个),加快启动速度和减少文件系统调用。如果你需要保护源码,考虑用 V8 字节码编译(bytenode)或 WebAssembly。
🚧 不签名的后果:Windows 上 SmartScreen 会拦截,macOS 上 Gatekeeper 会阻止运行。用户看到"未知发布者"警告,大概率不会安装。签名是分发的必经步骤。
📌 electron-updater 的工作原理:它从你指定的 URL(如 GitHub Releases)下载 latest.yml(Windows)或 latest-mac.yml(macOS),对比版本号,如果有新版就下载增量更新包。你只需要每次发版把安装包和 yml 文件上传到 Releases。
📌 Spectron 已废弃。Electron 官方推荐用 Playwright 的 Electron 支持来写 E2E 测试。Playwright 可以启动 Electron 应用、模拟用户操作、截图、断言 DOM。
📌 什么时候需要原生模块?当你需要调用系统底层 API(如 Windows Registry、macOS Keychain)、复用 C/C++ 库(如 OpenCV)、或做高性能计算时。但原生模块会增加编译复杂度(需要为每个平台编译),能用纯 JS 解决的就别用原生模块。
| API | 说明 | 示例 |
|---|---|---|
app.whenReady() |
应用就绪 | app.whenReady().then(init) |
app.quit() |
退出应用 | app.quit() |
app.getPath(name) |
获取系统路径 | app.getPath('userData') |
BrowserWindow |
创建窗口 | new BrowserWindow({...}) |
Menu.buildFromTemplate() |
构建菜单 | Menu.setApplicationMenu(menu) |
Tray |
系统托盘 | new Tray(icon) |
dialog.showOpenDialog() |
文件选择 | await dialog.showOpenDialog(...) |
dialog.showSaveDialog() |
保存对话框 | await dialog.showSaveDialog(...) |
dialog.showMessageBox() |
消息框 | await dialog.showMessageBox(...) |
globalShortcut.register() |
全局快捷键 | globalShortcut.register('Cmd+Shift+D', fn) |
clipboard.writeText() |
写剪贴板 | clipboard.writeText('hello') |
Notification |
系统通知 | new Notification({title, body}).show() |
powerMonitor |
电源监听 | powerMonitor.on('suspend', fn) |
screen.getAllDisplays() |
显示器信息 | screen.getAllDisplays() |
shell.openExternal() |
打开外部链接 | shell.openExternal('https://...') |
shell.showItemInFolder() |
在文件管理器中显示 | shell.showItemInFolder(path) |
| API | 进程 | 说明 |
|---|---|---|
ipcMain.handle(channel, handler) |
主 | 注册异步处理器 |
ipcMain.on(channel, handler) |
主 | 监听单向消息 |
ipcRenderer.invoke(channel, ...args) |
渲染 | 调用主进程(返回 Promise) |
ipcRenderer.send(channel, ...args) |
渲染 | 发送单向消息 |
webContents.send(channel, ...args) |
主 | 向渲染进程推送 |
contextBridge.exposeInMainWorld(name, api) |
Preload | 暴露安全 API |
| 选项 | 类型 | 说明 |
|---|---|---|
width / height |
number | 窗口尺寸 |
minWidth / minHeight |
number | 最小尺寸 |
x / y |
number | 窗口位置 |
frame |
boolean | 是否显示标题栏 |
transparent |
boolean | 是否透明 |
alwaysOnTop |
boolean | 是否置顶 |
fullscreen |
boolean | 是否全屏 |
resizable |
boolean | 是否可调整大小 |
parent |
BrowserWindow | 父窗口 |
modal |
boolean | 是否模态 |
webPreferences.preload |
string | preload 脚本路径 |
webPreferences.contextIsolation |
boolean | 上下文隔离 |
webPreferences.nodeIntegration |
boolean | 渲染进程 Node.js |
webPreferences.sandbox |
boolean | 沙箱模式 |
A: Electron 捆绑了完整的 Chromium 和 Node.js 运行时,基础包约 80-150MB。优化方法:
asar 打包减少文件数量prune: true 移除 devDependencieselectron-builder 的 compression: maximumrequire is not definedA: nodeIntegration: false 时渲染进程没有 Node.js。通过 preload + contextBridge 暴露需要的 API。
A: 不应该直接在渲染进程中用。正确做法:
preload 中 require 模块contextBridge.exposeInMainWorld 暴露封装后的 APIA: macOS 的惯例是关闭窗口不退出应用。如果你想要非 macOS 行为:
A: 打包后的图片路径会变化。使用协议处理:
A: 需要代码签名。使用 EV 代码签名证书可以立即建立声誉,OV 证书需要一段时间。
A: Electron 不支持移动端。如果需要移动端,考虑:
总结:Electron 的核心是"用 Web 技术写桌面应用"。理解三个关键概念就够了:主进程 vs 渲染进程(职责分离)、IPC 通信(进程间桥梁)、安全模型(contextIsolation + preload + contextBridge)。剩下的都是 Web 开发的老本行。