Electron开发重点

Electron 开发重点知识体系梳理

Electron 是基于 ChromiumNode.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.jsonmain 字段指定的文件(如 main.js
渲染进程 (Renderer Process) 页面渲染、UI 交互、业务逻辑执行 Chromium 环境(类似浏览器标签页) 每个 BrowserWindow 加载的 HTML/JS 文件

2. 关键模块与对象

  • app 模块:控制应用的生命周期(仅主进程可用)。
  • BrowserWindow 模块:创建和管理应用窗口(仅主进程可用)。
  • webContents 对象:渲染进程的核心对象,负责页面渲染和与渲染进程通信(主进程中通过 win.webContents 访问)。

二、进程间通信 (IPC) —— 开发核心难点

Electron 双进程模型下,主进程与渲染进程、渲染进程之间的通信是最频繁且最容易出错的环节,必须掌握以下模式:

1. 核心通信模块

  • **ipcMain**:主进程模块,用于监听渲染进程发送的消息。
  • **ipcRenderer**:渲染进程模块,用于向主进程发送消息、监听主进程回复。

2. 常用通信模式

(1)渲染进程 → 主进程(单向通知)

场景:渲染进程触发主进程执行某个操作(如打开文件选择框、创建新窗口)。

1
2
3
4
5
6
7
8
9
10
// 渲染进程 (renderer.js)
const { ipcRenderer } = require('electron');
ipcRenderer.send('open-file-dialog');

// 主进程 (main.js)
const { ipcMain, dialog } = require('electron');
ipcMain.on('open-file-dialog', async (event) => {
const result = await dialog.showOpenDialog({ properties: ['openFile'] });
console.log(result.filePaths);
});

(2)渲染进程 → 主进程 → 渲染进程(双向请求/响应)

场景:渲染进程请求主进程执行异步操作并返回结果(如读取本地文件、调用系统 API)。

1
2
3
4
5
6
7
8
9
10
11
12
// 渲染进程 (renderer.js)
const { ipcRenderer } = require('electron');
async function readFile() {
const content = await ipcRenderer.invoke('read-local-file', '/path/to/file.txt');
console.log(content);
}

// 主进程 (main.js)
const { ipcMain, fs } = require('electron');
ipcMain.handle('read-local-file', async (event, filePath) => {
return fs.promises.readFile(filePath, 'utf-8');
});

(3)主进程 → 渲染进程(主动推送)

场景:主进程监听系统事件(如托盘点击、网络变化),主动通知渲染进程更新 UI。

1
2
3
4
5
6
7
// 主进程 (main.js)
win.webContents.send('system-tray-clicked', { timestamp: Date.now() });

// 渲染进程 (renderer.js)
ipcRenderer.on('system-tray-clicked', (event, data) => {
console.log('托盘被点击:', data);
});

(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,宿主页面监听 webviewipc-message 事件 。 用于 webview 内页面向上通知宿主页面。
IPC (ipcRenderer) 宿主页面 webviewpreload 脚本 宿主页面调用 webview.send() 发送 IPC 消息 。 webview 内页面通过 preload 脚本暴露有限的、安全的 API,进行复杂或需要 Node.js 能力的通信。
preload 脚本 宿主页面 webviewpreload 脚本通过 ipcRenderer.sendToHost() 回复,宿主页面监听 webviewipc-message 事件 。 用于 preload 脚本向宿主页面发送消息。

方案一:基于 postMessage 的双向通信

这种方式最接近 Web 标准。关键在于宿主页面需要借助 executeJavaScriptwebview 的上下文中执行代码,从而建立连接。

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 的数据可能会受限。一个更稳健的模式是让宿主页面通过 executeJavaScriptwebview 内注入一个全局函数,该函数再通过 postMessage 将数据传回,而宿主则通过监听同一个 ipc-message 来捕获。不过,下面的“宿主发送给webview”方案通常更常用。

2. 宿主页面发送消息给 WebView 内页面

宿主页面通过 executeJavaScriptwebview 的上下文中执行代码,从而调用其内部的 postMessage 方法 。

  • 在宿主页面(渲染进程)中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const 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);
    // 处理来自宿主页面的消息
    });
方案二:基于 ipcRendererpreload 的双向通信

这种方式利用了 Electron 的进程间通信能力,功能更强大,但需要在 webview 上启用 nodeintegration 或通过 preload 脚本安全地暴露 API。

1. 宿主页面发送消息给 WebView(通过 webview.send

宿主页面调用 webview.send() 发送一个 IPC 消息,该消息可以在 webviewpreload 脚本中通过 ipcRenderer 接收 。

  • 在宿主页面中:

    1
    2
    3
    4
    5
    const 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
    8
    const { 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() 向宿主页面发送消息,宿主页面监听 webviewipc-message 事件 。

  • preload.js 中:

    1
    2
    3
    4
    5
    6
    const { ipcRenderer } = require('electron');

    // 假设在某个时机,需要通知宿主页面
    function notifyHost() {
    ipcRenderer.sendToHost('custom-event', { data: 'Something happened in webview' });
    }
  • 在宿主页面中:

    1
    2
    3
    4
    5
    6
    const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 主进程创建窗口时配置
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 禁用渲染进程 Node.js 集成
contextIsolation: true, // 启用上下文隔离(默认开启)
preload: path.join(__dirname, 'preload.js') // 预加载脚本
}
});

// preload.js(预加载脚本,在渲染进程上下文隔离前执行)
const { contextBridge, ipcRenderer } = require('electron');
// 向渲染进程暴露安全 API
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.send('open-file-dialog'),
readLocalFile: (filePath) => ipcRenderer.invoke('read-local-file', filePath),
onTrayClick: (callback) => ipcRenderer.on('system-tray-clicked', callback)
});

// 渲染进程 (renderer.js) 中直接使用暴露的 API
window.electronAPI.openFileDialog();

三、窗口与应用生命周期管理

1. BrowserWindow 窗口配置

创建窗口时的核心配置项(webPreferences 是安全重点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
frame: false, // 无边框窗口(自定义标题栏时用)
transparent: true, // 透明窗口(配合无边框使用)
resizable: true,
webPreferences: {
// 安全配置(必看)
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
// 开发环境配置
devTools: true // 生产环境建议禁用
}
});

// 加载页面
win.loadURL('https://example.com'); // 加载远程 URL
win.loadFile('index.html'); // 加载本地 HTML 文件

2. 窗口类型与层级

  • 父子窗口:通过 parent 选项创建,子窗口始终显示在父窗口上方。
    1
    const childWin = new BrowserWindow({ parent: win, modal: true });
  • 模态窗口:设置 modal: true,阻塞父窗口交互(常用于弹窗、对话框)。

对于showSaveDialogSync、showOpenDialogSync同样可以设置parent,确保对话框显示在主窗口上方。

3. 应用生命周期(app 模块事件)

掌握核心生命周期事件,控制应用启动、退出逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { app } = require('electron');

// 1. 应用准备就绪(创建窗口的最佳时机)
app.whenReady().then(() => {
createWindow();
// macOS 特性:点击 Dock 图标重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

// 2. 所有窗口关闭时(Windows/Linux 直接退出应用)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

// 3. 应用退出前(清理资源、保存数据)
app.on('before-quit', (event) => {
// 可在此阻止退出:event.preventDefault();
});

四、原生能力与系统交互

Electron 的核心优势是通过 Node.js 和内置模块调用原生系统能力,以下是高频使用场景:

1. 文件系统操作

主进程可直接使用 Node.js 的 fspath 模块,结合 dialog 模块实现文件选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { dialog } = require('electron');
const fs = require('fs/promises');

// 打开文件选择框
async function selectAndReadFile() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt'] }]
});
if (!canceled) {
const content = await fs.readFile(filePaths[0], 'utf-8');
return content;
}
}

2. 菜单与托盘

  • 应用菜单:通过 Menu 模块创建自定义菜单栏(Windows/Linux 在窗口顶部,macOS 在屏幕顶部)。
    1
    2
    3
    4
    5
    6
    7
    const { 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
    8
    const { 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 Notification API(渲染进程)或 Node.js 模块(主进程)。
  • 剪贴板:通过 clipboard 模块读写系统剪贴板。
  • 外部链接:通过 shell 模块在默认浏览器中打开链接(避免在 Electron 窗口中打开外部页面)。
    1
    2
    const { shell } = require('electron');
    shell.openExternal('https://github.com');

五、工程化:打包与分发

开发完成后,需将 Electron 应用打包为各平台的安装包(.exe.dmg.AppImage 等),**electron-builder** 是目前最主流的打包工具。

1. 快速配置

package.json 中添加 build 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build:win": "electron-builder --win", // 打包 Windows
"build:mac": "electron-builder --mac", // 打包 macOS
"build:linux": "electron-builder --linux" // 打包 Linux
},
"build": {
"appId": "com.example.myapp",
"productName": "My Electron App",
"directories": { "output": "dist" },
"win": { "target": "nsis", "icon": "build/icon.ico" },
"mac": { "target": "dmg", "icon": "build/icon.icns" },
"linux": { "target": "AppImage" }
}
}

windows签名

TODO

macOS签名

TODO

linux签名-统信UOS、麒麟

TODO

2. 自动更新

使用 electron-updater 实现应用自动更新(需配合静态文件服务器或 GitHub Releases):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 主进程中配置
const { autoUpdater } = require('electron-updater');

// 检查更新
app.whenReady().then(() => {
autoUpdater.checkForUpdatesAndNotify();
});

// 监听更新事件
autoUpdater.on('update-available', () => {
dialog.showMessageBox({ message: '发现新版本,正在下载...' });
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({ message: '更新下载完成,点击确定重启应用' }, () => {
autoUpdater.quitAndInstall();
});
});

3. 崩溃上报及分析

  • 崩溃上报:使用 electron-crash-reporterelectron-builder 自动上报崩溃信息。
  • 日志记录:在主进程中使用 electron-log 库记录运行时日志,方便调试。
    TODO

4. 日志上报

  • 日志记录:在主进程中使用 electron-log 库记录运行时日志,方便调试。
  • 日志上报:将日志上传到服务器,分析崩溃原因(如 阿里云Arms、Sentry、Loggly 等)。
    TODO

六、性能优化与安全

1. 性能优化重点

  • 渲染进程性能
    • 避免在主线程执行 heavy 计算,使用 Web Workers
    • 优化 DOM 操作,使用虚拟滚动(如 react-window)处理长列表。
    • 减少不必要的重绘重排,使用 CSS transformopacity 做动画。
  • 主进程性能
    • 避免在主进程执行阻塞操作(如同步文件读写),尽量使用异步 API。
    • 合理使用 BrowserWindowshow: false 预加载窗口,提升打开速度。
  • 内存管理
    • 及时关闭不再使用的窗口,避免内存泄漏。
    • 移除不再需要的 IPC 监听器(ipcRenderer.removeListener)。

2. 安全红线(必须遵守)

Electron 应用的安全漏洞可能导致远程代码执行,以下是强制安全配置

  1. **始终禁用 nodeIntegration**,启用 contextIsolation
  2. 使用 Preload 脚本 暴露 API,禁止直接在渲染进程使用 Node.js。
  3. 禁止加载不受信任的远程内容,若必须加载,启用 webSecurity(默认开启)。
  4. **设置 Content Security Policy (CSP)**,在 HTML 中添加:
    1
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  5. 避免使用 remote 模块(已弃用),改用 IPC 通信。

七、调试与常见问题

1. 调试技巧

  • 渲染进程调试:打开 DevTools(win.webContents.openDevTools()),和 Chrome 调试一致。
  • 主进程调试:使用 --inspect 启动应用,通过 Chrome DevTools 连接:
    1
    electron --inspect=5858 main.js
    然后在 Chrome 中访问 chrome://inspect 进行调试。

2. 常见坑点

  • 跨域问题:渲染进程加载远程接口时可能遇到 CORS,可在主进程通过 webRequest 模块修改响应头,或配置代理。
  • 路径问题:打包后文件路径变化,使用 path.join(__dirname, 'file.txt') 而非相对路径。
  • macOS 签名与公证:macOS 应用必须签名和公证才能在非开发者机器上运行,需在 electron-builder 中配置证书。

Electron开发重点
https://cszy.top/20260212-Electron开发重点/
作者
csorz
发布于
2026年2月12日
许可协议