1. Electron的运行原理是什么?
Electron的核心是Chromium + Node.js的组合。它将Chromium(负责渲染界面)和Node.js(提供操作系统底层访问能力)合并到同一个进程中。运行时分为主进程和渲染进程:主进程是入口,控制应用生命周期;渲染进程则展示UI。两者通过进程间通信(IPC)协同工作
2. 主进程和渲染进程的区别?各自能做什么?
| 对比项 | 主进程 (Main Process) | 渲染进程 (Renderer Process) |
|---|---|---|
| 数量 | 唯一(app 生命周期内一个实例) |
每个 BrowserWindow 对应一个独立渲染进程 |
| 入口 | main.js(通过 package.json 的 main 指定) |
每个窗口加载的 HTML 及其 JS |
| Node.js 环境 | 完全支持(require, fs, child_process 等) |
默认关闭(nodeIntegration: false,安全考虑) |
| 窗口管理 | 创建、控制、销毁 BrowserWindow |
不能创建新窗口,但可通过 IPC 请求主进程创建 |
| 系统原生 GUI | 可调用 Menu, Tray, Notification, dialog 等模块 |
不可直接调用,需通过 IPC 委托给主进程 |
| 生命周期 | 应用启动时创建,退出时销毁 | 窗口关闭时销毁,但可隐藏而非关闭 |
| 调试方式 | 命令行启动时使用 --inspect 或 --inspect-brk |
Chrome DevTools(Ctrl+Shift+I) |
| 进程间通信 | 通过 ipcMain 监听 / 回复 |
通过 ipcRenderer 发送 / 监听 |
3. 通讯方式(进程间通信 IPC)
Electron 基于 Chromium 的多进程架构,主进程 与渲染进程默认相互隔离,必须通过 IPC 通信。
3.1 核心模块
- 主进程 :
ipcMain - 渲染进程 :
ipcRenderer - 安全暴露 API :
contextBridge(配合预加载脚本)
3.2 常用通信模式
| 模式 | 方向 | API | 特点 |
|---|---|---|---|
| 异步发送(单向) | 渲染 → 主 | ipcRenderer.send + ipcMain.on |
不等待回应 |
| 异步请求-回复(双向) | 渲染 → 主 → 渲染 | ipcRenderer.invoke + ipcMain.handle |
推荐,返回 Promise |
| 同步发送(阻塞) | 渲染 → 主 | ipcRenderer.sendSync |
阻塞渲染进程,不推荐 |
| 主进程主动发消息 | 主 → 特定渲染 | win.webContents.send(channel, data) |
需要获取窗口对象 |
| 主进程广播 | 主 → 所有渲染 | 遍历 BrowserWindow.getAllWindows() 依次发送 |
自定义实现 |
2.3 ipc 通信示例
步骤1:编写预加载脚本
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 获取应用版本(异步调用主进程)
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
// 保存文件(暴露文件保存能力)
saveFile: (content) => ipcRenderer.invoke('save-file', content),
// 监听来自主进程的消息(如系统主题变化)
onThemeChange: (callback) => {
ipcRenderer.on('theme-changed', (event, newTheme) => callback(newTheme));
},
// 移除监听,避免内存泄漏
removeThemeListener: () => {
ipcRenderer.removeAllListeners('theme-changed');
}
});
// 可选:在 preload 中做一些初始化,但不要操作 DOM(因为页面还未加载)
console.log('Preload script loaded');
步骤2:在主进程中引用预加载脚本
javascript
// main.js
const { BrowserWindow } = require('electron');
const path = require('path');
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// 务必保持 false(安全)
nodeIntegration: false,
contextIsolation: true
}
});
// 主进程处理消息
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
ipcMain.handle('save-file', async (event, content) => {
// 使用 fs 写入文件
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(app.getPath('userData'), 'myfile.txt');
await fs.writeFile(filePath, content);
return filePath;
});
// 主进程主动发消息(例如改变主题)
win.webContents.on('did-finish-load', () => {
win.webContents.send('theme-changed', 'dark');
});
步骤3:在渲染进程中使用暴露的 API
javascript
// renderer.js (运行在网页中)
(async () => {
// 调用暴露的方法
const version = await window.electronAPI.getAppVersion();
console.log('App version:', version);
document.getElementById('saveBtn').addEventListener('click', async () => {
const path = await window.electronAPI.saveFile('Hello Electron');
console.log('File saved at:', path);
});
// 监听主进程消息
window.electronAPI.onThemeChange((newTheme) => {
document.body.className = `theme-${newTheme}`;
});
})();
3.4 渲染进程之间通信
- 通过主进程转发:一个渲染进程发消息给主进程,主进程再转发给另一个渲染进程。
- Web 原生方法 :
MessageChannel/BroadcastChannel(同源窗口可用,但注意 Electron 多窗口可能跨 webContents,推荐前者)。
4. 预加载脚本和普通渲染进程脚本有什么区别?
| 对比项 | 预加载脚本 | 普通渲染进程脚本 |
|---|---|---|
| 执行时间 | HTML 加载前 | HTML 解析过程中或之后 |
| 可访问 Node.js | 是(通过 require)只有一部分NodeAPI可使用 |
否(默认 nodeIntegration: false) |
| 环境隔离 | 运行在"隔离世界",通过 contextBridge 暴露 API给渲染进程 |
运行在"主要世界"(即正常网页的 window) |
| 是否可以操作 DOM | 技术上可以,但不推荐 | 完全可以(常规前端代码) |
4.1 关键安全原则
| 原则 | 说明 |
|---|---|
| 永不直接暴露 Node.js 模块 | 不要 exposeInMainWorld('fs', require('fs')) |
| 验证所有输入参数 | 防止路径遍历、命令注入 |
保持 contextIsolation: true |
隔离预加载环境与前端 window 对象 |
| 最小化暴露 API | 只暴露必需的方法 |
4.2 常见面试点
- 执行时机:渲染进程创建后、HTML 加载前。
- 与普通渲染脚本区别:预加载脚本拥有 Node.js 权限,普通脚本没有。
- 多个窗口:可以共享同一个预加载脚本,但每个窗口独立实例。
- 操作 DOM :技术上可以,但推荐用
DOMContentLoaded事件,或者交给普通渲染脚本去做。
五、优化方式(性能与体验)
5.1 启动速度优化
- 懒加载窗口 :
ready-to-show事件后再show()窗口,避免白屏。 - 使用
backgroundThrottling: false:防止隐藏页面被节流(需要时)。 - 预加载优化:预加载脚本保持精简,不要执行耗时同步操作。
- 模块按需引入 :
require时动态加载,避免启动时加载全部依赖。
5.2 运行时性能优化
- 启用
nodeIntegration: false:避免渲染进程直接访问 Node.js,降低安全风险并减少上下文切换开销。 - 合理使用 Web Workers:CPU 密集任务放到 Worker 线程。
- 避免内存泄漏 :及时移除事件监听、销毁不用的窗口 (
destroy())、使用WeakMap管理缓存。 - 限制渲染帧率 :使用
requestAnimationFrame控制不必要的 UI 刷新。 - 使用
requestIdleCallback或任务分片 - 使用
will-navigate等事件拦截不必要的导航。 - 减少不必要的 IPC 调用
5.3 渲染优化(同常规 Web 优化)
- 使用 Chrome DevTools 分析重排/重绘。
- 大列表使用虚拟滚动(如
react-window)。 - 图片懒加载、压缩。
- 防止 JavaScript 长时间阻塞主线程。
六、打包体积大小控制
Electron 应用体积大的原因:内置了完整 Chromium + Node.js (约 60--80MB 基础体积)。额外体积来自于 node_modules 和源码。
6.1 常用瘦身手段
| 方法 | 效果 | 备注 |
|---|---|---|
使用 electron-builder 的 asar 打包 |
减少文件数量,不影响体积 | 必须开启 |
排除无用模块(devDependencies) |
可减少几十 MB | 在打包配置中 node_modules 过滤 |
删除不必要的二进制文件(如 .node 原生模块中多余的平台文件) |
数 MB | 手动清理或使用 electron-builder 的 afterPack 钩子 |
使用 webpack / vite 打包前端代码 |
减少冗余代码,合并文件 | 显著减小 app.asar 体积 |
不打包源映射文件(.map) |
减小 20-40% | 生产环境关闭 devtool |
| 升级 Electron 版本 | 新版本有时会优化基础库大小 | 评估兼容性 |
使用 electron-builder 的 file 配置精细控制哪些文件进入 |
精准瘦身 | 可排除测试、文档、示例等 |
6.2 典型打包配置 (electron-builder)
json
json
{
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"renderer/**/*",
"!**/*.map",
"!**/test/**"
],
"asar": true,
"nodeModules": ["prod"] // 仅打包生产依赖
}
6.3 体积对比参考
- 空白 Electron 应用打包后大约 70--90 MB(Windows exe)。
- 优化后(剔除多余依赖、压缩前端代码)约 50--70 MB。
- 极致优化(使用
electron-builder+ 7z 压缩 + 增量更新)最小可达 40 MB 左右。
七、常用系统 API 模块详解
7.1. 通知(Notifications)
用于从应用向操作系统发送原生桌面通知。
-
使用位置:主进程和渲染进程均可使用,但实现方式不同。
-
API 模块 :主进程使用
Notification模块;渲染进程则直接使用 HTML5 的NotificationAPI。 -
代码示例:
dart// 主进程 new Notification({ title: '标题', body: '这是通知的内容', icon: './icon.png' }).show();javascript// 渲染进程 new window.Notification('标题', { body: '这是通知的内容' });
7.2. 系统托盘(Tray)
允许应用在操作系统的通知区域(Windows 任务栏右侧或 macOS 菜单栏右侧)显示一个图标,并提供右键菜单和交互功能。
-
使用位置:仅限于主进程。
-
API 模块 :
Tray模块。 -
注意事项 :在 macOS 上,建议使用"模板图片"(Template Image)作为图标,这样图标可以自动适应浅色和深色模式的菜单栏。托盘图标的创建需要在应用
ready事件之后。 -
代码示例:
iniconst { app, Tray, Menu } = require('electron'); let tray = null; app.whenReady().then(() => { tray = new Tray('./path/to/icon.png'); const contextMenu = Menu.buildFromTemplate([ { label: '显示应用', click: () => { win.show(); } }, { label: '退出', click: () => { app.quit(); } } ]); tray.setContextMenu(contextMenu); tray.setToolTip('我的应用'); });
7.3. 对话框(Dialogs)
用于调用系统原生的对话框,例如:打开文件、保存文件、消息提示等。
-
使用位置:主进程。
-
API 模块 :
dialog模块。 -
常见 API:
dialog.showOpenDialogSync(): 同步打开文件选择对话框。dialog.showSaveDialog(): 打开保存文件对话框。dialog.showMessageBox(): 打开一个消息提示框。
-
注意事项 :因为对话框会阻塞窗口,建议使用异步方法如
showOpenDialog,以免 UI 卡死。showMessageBox可用于确认等操作,返回值会包含用户点击的按钮索引。
7.4. 剪贴板(Clipboard)
用于读写系统剪贴板上的文本、图片、文件等数据。
-
使用位置:主进程和渲染进程均可使用。
-
API 模块 :
clipboard模块。 -
代码示例:
arduinoconst { clipboard } = require('electron'); clipboard.writeText('Hello, Electron!'); const text = clipboard.readText(); console.log(text); // 输出: Hello, Electron!
7.5. 全局快捷键(Global Shortcuts)
允许应用注册系统级别的快捷键,即使应用在后台或没有焦点,也能响应用户的按键组合。
-
使用位置:主进程。
-
API 模块 :
globalShortcut模块。 -
注意事项 :必须在应用
ready事件之后才能注册。为避免冲突,建议在应用退出时注销所有已注册的快捷键。 -
代码示例:
javascriptconst { app, globalShortcut } = require('electron'); app.whenReady().then(() => { globalShortcut.register('CommandOrControl+X', () => { console.log('CommandOrControl+X is pressed'); }); }); app.on('will-quit', () => { globalShortcut.unregisterAll(); });
7.6. 电源管理(Power Management)
用于监控系统电源状态,或阻止系统进入睡眠模式(例如在下载或播放视频时)。
-
使用位置:主进程。
-
API 模块 :
powerMonitor和powerSaveBlocker模块。 -
常见 API:
powerMonitor.getSystemIdleState(): 获取系统空闲状态。powerSaveBlocker.start(): 开始阻止系统睡眠。powerSaveBlocker.stop(id): 停止阻止系统睡眠。
-
powerSaveBlocker使用场景:prevent-app-suspension: 阻止应用被挂起(例如后台下载时)。prevent-display-sleep: 阻止显示器关闭(例如全屏播放视频时),后者的优先级高于前者。
7.7. 菜单(Menu)
用于创建自定义的应用程序菜单(位于窗口标题栏下方),取代系统默认的 Electron 菜单。
-
使用位置:主进程。
-
API 模块 :
Menu模块。 -
常见 API:
Menu.setApplicationMenu(): 设置应用程序的菜单栏。Menu.buildFromTemplate(): 通过模板构建菜单。
7.8. 原生主题(Native Theme)
用于获取和监听操作系统的原生主题(浅色/深色模式)变化。
-
使用位置:主进程和渲染进程。
-
API 模块 :
nativeTheme模块。 -
常见 API:
nativeTheme.shouldUseDarkColors: 检查当前是否使用深色主题。nativeTheme.on('updated'): 监听主题变化事件。
7.9. 其他系统交互
- 屏幕信息 :使用
screen模块可以获取系统显示器的信息,如尺寸、分辨率、DPI 缩放等。 - 打印 :通过
webContents.print()或webContents.printToPDF()实现网页打印或将网页保存为 PDF。 - 网络状态 :使用
net模块可以发起原生 HTTP/HTTPS 请求,netLog模块用于网络请求日志记录。