Electron开发重点
Electron 开发重点知识体系梳理
Electron 是基于 Chromium 和 Node.js 的跨平台桌面应用开发框架,核心逻辑是用 Web 技术(HTML/CSS/JS)开发界面,同时通过 Node.js 调用原生系统能力。以下是从基础架构到工程化落地的完整知识体系梳理。
项目汇总
code/all/e-streamlabs-obs-v0.21.2x 导播助手
code/all/e-live(已下线)
code/all/e-ppt(已下线)
code/all/e-live-streaming 直播助手
code/all/e-ipub 融合出版平台
code/all/e-ysy 亿童幼师云
一、核心架构与基础概念
1. 双进程模型(核心基石)
Electron 应用由两类进程组成,二者职责严格分离,通过 IPC 通信协作:
| 进程类型 | 核心职责 | 运行环境 | 入口文件 |
|---|---|---|---|
| 主进程 (Main Process) | 应用生命周期管理、窗口创建、原生系统调用、全局状态管理 | Node.js 环境 | package.json 中 main 字段指定的文件(如 main.js) |
| 渲染进程 (Renderer Process) | 页面渲染、UI 交互、业务逻辑执行 | Chromium 环境(类似浏览器标签页) | 每个 BrowserWindow 加载的 HTML/JS 文件 |
2. 关键模块与对象
app模块:控制应用的生命周期(仅主进程可用)。BrowserWindow模块:创建和管理应用窗口(仅主进程可用)。webContents对象:渲染进程的核心对象,负责页面渲染和与渲染进程通信(主进程中通过win.webContents访问)。
二、进程间通信 (IPC) —— 开发核心难点
Electron 双进程模型下,主进程与渲染进程、渲染进程之间的通信是最频繁且最容易出错的环节,必须掌握以下模式:
1. 核心通信模块
- **
ipcMain**:主进程模块,用于监听渲染进程发送的消息。 - **
ipcRenderer**:渲染进程模块,用于向主进程发送消息、监听主进程回复。
2. 常用通信模式
(1)渲染进程 → 主进程(单向通知)
场景:渲染进程触发主进程执行某个操作(如打开文件选择框、创建新窗口)。
1 | |
(2)渲染进程 → 主进程 → 渲染进程(双向请求/响应)
场景:渲染进程请求主进程执行异步操作并返回结果(如读取本地文件、调用系统 API)。
1 | |
(3)主进程 → 渲染进程(主动推送)
场景:主进程监听系统事件(如托盘点击、网络变化),主动通知渲染进程更新 UI。
1 | |
(4)渲染进程 <-> Webview进程
场景:渲染进程与 Webview 进程之间需要通信(如 Webview 加载完成后,渲染进程通知 Webview 执行某个操作)。
postMessage 方法:渲染进程通过 webContents.postMessage 发送消息,Webview 进程通过 window.addEventListener('message', (event) => { ... }) 监听。
在 Electron 中,<webview> 标签内的“访客”内容(即加载的第三方网页)运行在一个独立的、隔离的渲染进程中,拥有自己的 window 对象和执行环境。因此,它无法直接访问您主应用渲染进程(宿主页面)的全局对象或 IPC 通道。
核心在于渲染进程(宿主页面)如何与 webview 中的内容通信。主要有两种方式:一种是基于 postMessage 的标准 Web API,另一种是基于 Electron IPC 的专用方法。如果您希望通信双方代码耦合度低、逻辑通用,postMessage 是更合适的选择。
以下是两种通信方式的详细实现和对比:
| 通信方式 | 发送方 | 接收方 | 核心机制 | 典型使用场景 |
|---|---|---|---|---|
postMessage |
宿主页面 | webview 内页面 |
宿主页面通过 webview.executeJavaScript() 注入脚本,调用 window.postMessage 。 |
通信双方都需要监听标准的 message 事件,代码逻辑与普通网页一致。 |
webview 内页面 |
宿主页面 | webview 内页面直接调用 window.postMessage,宿主页面监听 webview 的 ipc-message 事件 。 |
用于 webview 内页面向上通知宿主页面。 |
|
| IPC (ipcRenderer) | 宿主页面 | webview 内 preload 脚本 |
宿主页面调用 webview.send() 发送 IPC 消息 。 |
webview 内页面通过 preload 脚本暴露有限的、安全的 API,进行复杂或需要 Node.js 能力的通信。 |
preload 脚本 |
宿主页面 | webview 内 preload 脚本通过 ipcRenderer.sendToHost() 回复,宿主页面监听 webview 的 ipc-message 事件 。 |
用于 preload 脚本向宿主页面发送消息。 |
方案一:基于 postMessage 的双向通信
这种方式最接近 Web 标准。关键在于宿主页面需要借助 executeJavaScript 在 webview 的上下文中执行代码,从而建立连接。
1. WebView 内页面发送消息给宿主页面
这是最直接的方式。webview 内部的网页像平时一样使用 postMessage,宿主页面通过监听 webview 标签的 ipc-message 事件来接收。
在
webview内的页面(访客页)中:1
2
3
4
5// 这是 webview 内加载的网页中的代码
function sendMessageToHost() {
// 直接向父窗口发送消息
window.postMessage({ type: 'FROM_WEBVIEW', text: 'Hello from inside!' }, '*');
}在宿主页面(渲染进程)中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 这是包含 <webview> 标签的页面代码
const webview = document.querySelector('webview');
webview.addEventListener('ipc-message', (event) => {
// 注意:通过 ipc-message 接收到的 event 对象结构与标准 MessageEvent 不同。
// 实际通过 postMessage 发送的复杂数据对象,需要通过其他方式(如 executeJavaScript 的回调)才能完整获取。
// 一个更可靠的方式是结合 executeJavaScript 在 webview 内设置监听器。
console.log('Received from webview:', event.channel, event.args);
});
// 更推荐的方式:通过 executeJavaScript 在 webview 内部设立监听器
webview.addEventListener('dom-ready', () => {
webview.executeJavaScript(`
window.addEventListener('message', (event) => {
// 可以将接收到的消息通过其他方式转发给宿主,例如:
console.log('Webview内部收到消息:', event.data);
// 但要将此消息发回给宿主,仍需依赖下面的“宿主页面发送消息给WebView内页面”的方法。
});
`);
});说明:直接通过
ipc-message接收postMessage的数据可能会受限。一个更稳健的模式是让宿主页面通过executeJavaScript在webview内注入一个全局函数,该函数再通过postMessage将数据传回,而宿主则通过监听同一个ipc-message来捕获。不过,下面的“宿主发送给webview”方案通常更常用。
2. 宿主页面发送消息给 WebView 内页面
宿主页面通过 executeJavaScript 在 webview 的上下文中执行代码,从而调用其内部的 postMessage 方法 。
在宿主页面(渲染进程)中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const webview = document.querySelector('webview');
function sendMessageToWebview() {
if (webview) {
const message = { type: 'FROM_HOST', text: 'Hello from host page!' };
// 关键步骤:构造一段脚本,在 webview 内部执行 window.postMessage
const script = `window.postMessage(${JSON.stringify(message)}, '*');`;
webview.executeJavaScript(script);
}
}
// 确保 webview 已加载完成
webview.addEventListener('dom-ready', () => {
// 在 webview 内部设置一个监听器,以接收来自外部的消息
webview.executeJavaScript(`
window.addEventListener('message', (event) => {
console.log('Webview received:', event.data);
// 在这里处理接收到的消息
if (event.data.type === 'FROM_HOST') {
// ... 执行相关操作
}
});
`);
});在
webview内的页面(访客页)中:1
2
3
4
5// 这是 webview 内加载的网页中的代码,它只需要监听标准的 message 事件即可
window.addEventListener('message', (event) => {
console.log('Message received from host:', event.data);
// 处理来自宿主页面的消息
});
方案二:基于 ipcRenderer 和 preload 的双向通信
这种方式利用了 Electron 的进程间通信能力,功能更强大,但需要在 webview 上启用 nodeintegration 或通过 preload 脚本安全地暴露 API。
1. 宿主页面发送消息给 WebView(通过 webview.send)
宿主页面调用 webview.send() 发送一个 IPC 消息,该消息可以在 webview 的 preload 脚本中通过 ipcRenderer 接收 。
在宿主页面中:
1
2
3
4
5const webview = document.querySelector('webview');
webview.addEventListener('dom-ready', () => {
// 向 webview 发送 IPC 消息,频道名为 'ping'
webview.send('ping', 'Hello from host!');
});在
preload.js(为webview指定的预加载脚本)中:1
2
3
4
5
6
7
8const { ipcRenderer } = require('electron');
ipcRenderer.on('ping', (event, message) => {
console.log('Received ping in preload:', message); // 输出: Received ping in preload: Hello from host!
// 可以向宿主页面回复消息
ipcRenderer.sendToHost('pong', 'Message received in webview!');
});
2. WebView 内页面发送消息给宿主页面(通过 sendToHost)
webview 内的 preload 脚本可以通过 ipcRenderer.sendToHost() 向宿主页面发送消息,宿主页面监听 webview 的 ipc-message 事件 。
在
preload.js中:1
2
3
4
5
6const { ipcRenderer } = require('electron');
// 假设在某个时机,需要通知宿主页面
function notifyHost() {
ipcRenderer.sendToHost('custom-event', { data: 'Something happened in webview' });
}在宿主页面中:
1
2
3
4
5
6const webview = document.querySelector('webview');
webview.addEventListener('ipc-message', (event) => {
if (event.channel === 'custom-event') {
console.log('Received from webview preload:', event.args[0]); // 输出: Received from webview preload: { data: 'Something happened in webview' }
}
});
(5)主进程 <-> Webview进程
同(3)主进程 → 渲染进程(主动推送)
(6)Webview进程 <-> Webview进程
多标签实现时,每个标签页都是一个独立的 Webview 进程,它们之间通过渲染进程中转通信。渲染进程作为中间层使用event-mitter库,负责接收来自 Webview 进程的消息,并将其转发给其他 Webview 进程。
3. 上下文隔离下的安全通信(现代 Electron 标准做法)
为了安全,**现代 Electron 必须启用 contextIsolation 和禁用 nodeIntegration**,此时渲染进程无法直接访问 ipcRenderer,需通过 Preload 脚本 暴露安全 API:
1 | |
三、窗口与应用生命周期管理
1. BrowserWindow 窗口配置
创建窗口时的核心配置项(webPreferences 是安全重点):
1 | |
2. 窗口类型与层级
- 父子窗口:通过
parent选项创建,子窗口始终显示在父窗口上方。1
const childWin = new BrowserWindow({ parent: win, modal: true }); - 模态窗口:设置
modal: true,阻塞父窗口交互(常用于弹窗、对话框)。
对于showSaveDialogSync、showOpenDialogSync同样可以设置parent,确保对话框显示在主窗口上方。
3. 应用生命周期(app 模块事件)
掌握核心生命周期事件,控制应用启动、退出逻辑:
1 | |
四、原生能力与系统交互
Electron 的核心优势是通过 Node.js 和内置模块调用原生系统能力,以下是高频使用场景:
1. 文件系统操作
主进程可直接使用 Node.js 的 fs、path 模块,结合 dialog 模块实现文件选择:
1 | |
2. 菜单与托盘
- 应用菜单:通过
Menu模块创建自定义菜单栏(Windows/Linux 在窗口顶部,macOS 在屏幕顶部)。1
2
3
4
5
6
7const { Menu } = require('electron');
const template = [
{ label: '文件', submenu: [{ label: '打开', click: () => console.log('打开文件') }] },
{ label: '编辑', submenu: [{ role: 'copy' }, { role: 'paste' }] } // 使用内置 role
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu); - 系统托盘:通过
Tray模块添加系统托盘图标和右键菜单。1
2
3
4
5
6
7
8const { Tray, Menu } = require('electron');
const tray = new Tray('path/to/icon.png');
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => win.show() },
{ label: '退出', click: () => app.quit() }
]);
tray.setToolTip('My Electron App');
tray.setContextMenu(contextMenu);
3. 系统集成
- 通知:使用 HTML5
NotificationAPI(渲染进程)或 Node.js 模块(主进程)。 - 剪贴板:通过
clipboard模块读写系统剪贴板。 - 外部链接:通过
shell模块在默认浏览器中打开链接(避免在 Electron 窗口中打开外部页面)。1
2const { shell } = require('electron');
shell.openExternal('https://github.com');
五、工程化:打包与分发
开发完成后,需将 Electron 应用打包为各平台的安装包(.exe、.dmg、.AppImage 等),**electron-builder** 是目前最主流的打包工具。
1. 快速配置
在 package.json 中添加 build 配置:
1 | |
windows签名
TODO
macOS签名
TODO
linux签名-统信UOS、麒麟
TODO
2. 自动更新
使用 electron-updater 实现应用自动更新(需配合静态文件服务器或 GitHub Releases):
1 | |
3. 崩溃上报及分析
- 崩溃上报:使用
electron-crash-reporter或electron-builder自动上报崩溃信息。 - 日志记录:在主进程中使用
electron-log库记录运行时日志,方便调试。
TODO
4. 日志上报
- 日志记录:在主进程中使用
electron-log库记录运行时日志,方便调试。 - 日志上报:将日志上传到服务器,分析崩溃原因(如 阿里云Arms、Sentry、Loggly 等)。
TODO
六、性能优化与安全
1. 性能优化重点
- 渲染进程性能:
- 避免在主线程执行 heavy 计算,使用
Web Workers。 - 优化 DOM 操作,使用虚拟滚动(如
react-window)处理长列表。 - 减少不必要的重绘重排,使用 CSS
transform和opacity做动画。
- 避免在主线程执行 heavy 计算,使用
- 主进程性能:
- 避免在主进程执行阻塞操作(如同步文件读写),尽量使用异步 API。
- 合理使用
BrowserWindow的show: false预加载窗口,提升打开速度。
- 内存管理:
- 及时关闭不再使用的窗口,避免内存泄漏。
- 移除不再需要的 IPC 监听器(
ipcRenderer.removeListener)。
2. 安全红线(必须遵守)
Electron 应用的安全漏洞可能导致远程代码执行,以下是强制安全配置:
- **始终禁用
nodeIntegration**,启用contextIsolation。 - 使用 Preload 脚本 暴露 API,禁止直接在渲染进程使用 Node.js。
- 禁止加载不受信任的远程内容,若必须加载,启用
webSecurity(默认开启)。 - **设置 Content Security Policy (CSP)**,在 HTML 中添加:
1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> - 避免使用
remote模块(已弃用),改用 IPC 通信。
七、调试与常见问题
1. 调试技巧
- 渲染进程调试:打开 DevTools(
win.webContents.openDevTools()),和 Chrome 调试一致。 - 主进程调试:使用
--inspect启动应用,通过 Chrome DevTools 连接:然后在 Chrome 中访问1
electron --inspect=5858 main.jschrome://inspect进行调试。
2. 常见坑点
- 跨域问题:渲染进程加载远程接口时可能遇到 CORS,可在主进程通过
webRequest模块修改响应头,或配置代理。 - 路径问题:打包后文件路径变化,使用
path.join(__dirname, 'file.txt')而非相对路径。 - macOS 签名与公证:macOS 应用必须签名和公证才能在非开发者机器上运行,需在
electron-builder中配置证书。