【前端】【Electron】Electron 知识点详解,看着一篇文章就够了
Electron 知识点详解第一章:Electron 入门与核心概念什么是 Electron?为什么选择 Electron?Electron 的主要挑战/缺点:核心架构:主进程 (Main Process) 与渲染进程 (Renderer Process)第二章:环境搭建与基础项目环境要求:创建基础项目: 关键配置: (主进程) 基础代码: (渲染进程) 基础代码:启动与调试:第三章:主进程 (M
Electron 知识点详解
第一章:Electron 入门与核心概念
-
什么是 Electron?
- 定义:一个使用 Web 技术 (HTML, CSS, JavaScript) 构建跨平台桌面应用程序的开源框架。
- 核心组成:Chromium (用于渲染界面) + Node.js (用于访问操作系统和后端能力) + 自定义 APIs。
- 目标:让 Web 开发者能够轻松创建功能丰富的桌面应用。
-
为什么选择 Electron?
- 跨平台: 一套代码库,可构建 Windows, macOS, Linux 应用。
- Web 技术栈: 复用现有的 Web 开发知识和生态系统 (NPM 包、框架如 Vue/React/Angular)。
- 快速开发: 利用 Web 技术的开发效率。
- 强大的能力: 可以访问完整的操作系统 API (通过 Node.js) 和 Electron 提供的原生 API。
- 成熟的社区和生态: 广泛使用 (VS Code, Slack, Discord 等),拥有丰富的文档和第三方库。
-
Electron 的主要挑战/缺点:
- 包体积大: 每个应用都内嵌了 Chromium 和 Node.js,导致基础包体积较大 (几十 MB 到上百 MB)。
- 内存占用: 相较于原生应用,内存占用可能更高。
- 性能: 对于极其注重性能的场景,可能不如原生应用。
- 安全风险: 如果不注意,将 Node.js 能力暴露给渲染进程可能带来安全风险。
-
核心架构:主进程 (Main Process) 与渲染进程 (Renderer Process)
- 主进程 (Main Process):
- 唯一,程序的入口点 (
main.js
或package.json
中指定的入口文件)。 - 拥有完整的 Node.js 环境。
- 负责管理应用的生命周期、创建和管理
BrowserWindow
(渲染进程)、处理原生操作系统交互 (菜单、对话框、托盘等)。 - 不负责渲染 HTML/CSS。
- 唯一,程序的入口点 (
- 渲染进程 (Renderer Process):
- 每个
BrowserWindow
实例拥有一个独立的渲染进程。 - 本质上是一个 Chromium 浏览器窗口环境,负责渲染 HTML, CSS, 执行 JavaScript (UI 逻辑)。
- 默认情况下不能直接访问 Node.js API 或操作系统资源 (出于安全考虑)。
- 可以通过特定的机制 (IPC, Preload Script) 与主进程通信以获取系统能力。
- 每个
- 理解进程模型是掌握 Electron 的关键。
- 主进程 (Main Process):
第二章:环境搭建与基础项目
-
环境要求:
- Node.js (自带 npm 或使用 yarn)
- 代码编辑器 (如 VS Code)
-
创建基础项目:
- 创建项目目录:
mkdir my-electron-app && cd my-electron-app
- 初始化 npm 项目:
npm init -y
- 安装 Electron:
npm install --save-dev electron
- 创建入口文件 (
main.js
) 和界面文件 (index.html
)。
- 创建项目目录:
-
package.json
关键配置:"main"
: 指定主进程入口文件 (e.g.,"main": "main.js"
)。"scripts"
:"start": "electron ."
: 定义启动应用的命令。
-
main.js
(主进程) 基础代码:- 引入
app
和BrowserWindow
模块:const { app, BrowserWindow } = require('electron')
- 创建窗口函数
createWindow()
。 - 在
app
的ready
事件触发时调用createWindow()
。 - 加载 HTML 文件:
win.loadFile('index.html')
或win.loadURL('http://localhost:3000')
(用于加载开发服务器)。 - 处理应用生命周期事件 (如
window-all-closed
,activate
)。
- 引入
-
index.html
(渲染进程) 基础代码:- 标准的 HTML 结构。
- 可以通过
<script src="renderer.js"></script>
引入渲染进程的 JavaScript 文件。
-
启动与调试:
- 启动应用:
npm start
- 打开开发者工具:在
BrowserWindow
实例上调用win.webContents.openDevTools()
。
- 启动应用:
第三章:主进程 (Main Process) 详解
-
app
模块:- 控制应用程序的事件生命周期。
- 常用事件:
ready
,window-all-closed
,activate
,before-quit
,will-quit
。 - 常用方法:
app.quit()
,app.getPath(name)
(获取系统路径),app.getName()
,app.getVersion()
,app.isPackaged
。
-
BrowserWindow
模块:- 创建和控制浏览器窗口。
- 构造函数选项 (
new BrowserWindow({...})
):width
,height
: 窗口尺寸。x
,y
: 窗口位置。frame
: 是否显示窗口边框和标题栏。show
: 创建时是否立即显示。webPreferences
: 配置网页功能的关键选项 (见下)。
- 实例方法:
win.loadURL()
,win.loadFile()
,win.close()
,win.show()
,win.hide()
,win.maximize()
,win.minimize()
,win.isMaximized()
,win.webContents
(访问 WebContents 对象)。 - 实例事件:
closed
,focus
,blur
,resize
,move
。
-
webPreferences
选项 (在BrowserWindow
中配置):nodeIntegration
(boolean, 默认false
): 是否在渲染进程中启用 Node.js 集成。强烈建议保持false
以提高安全性。contextIsolation
(boolean, 默认true
): 是否启用上下文隔离。强烈建议保持true
。这使得preload
脚本和渲染进程的 JavaScript 运行在不同的上下文中,更安全。preload
(string): 指定一个预加载脚本的路径。该脚本在渲染进程加载网页之前运行,并且可以访问 Node.js API (即使nodeIntegration: false
) 和 DOM API。这是连接主进程和渲染进程、安全暴露特定 Node.js 功能的关键。sandbox
(boolean, 默认false
): 是否启用 Chromium OS 级别的沙盒。
第四章:渲染进程 (Renderer Process) 详解
-
角色:
- 负责展示用户界面 (HTML/CSS)。
- 执行用户界面的交互逻辑 (JavaScript)。
- 运行标准的 Web API (Fetch, DOM 操作, Canvas 等)。
-
访问 Node.js (不推荐直接开启
nodeIntegration
):- 安全隐患: 如果
nodeIntegration: true
,渲染进程中的任何脚本 (包括第三方库) 都可以访问文件系统、执行命令等,容易受到 XSS 攻击影响。 - 推荐方式: 使用
preload
脚本 +contextBridge
。
- 安全隐患: 如果
-
preload.js
脚本:- 在
webPreferences
中通过preload
选项指定。 - 运行在具有 Node.js 环境但与渲染器隔离的上下文中 (当
contextIsolation: true
)。 - 可以访问
window
和document
对象。 - 主要用途:
- 使用
contextBridge.exposeInMainWorld(apiKey, apiObject)
安全地向渲染进程暴露选择性的 Node.js 功能或 IPC 调用接口。 - 监听来自主进程的 IPC 消息。
- 使用
- 在
-
renderer.js
(渲染进程脚本):- 通过
<script>
标签在 HTML 中引入。 - 负责 DOM 操作、事件处理、调用
preload
脚本暴露的 API。 - 如果使用了
contextBridge
,可以通过window[apiKey]
访问暴露的接口。
- 通过
第五章:进程间通信 (Inter-Process Communication - IPC)
-
为什么需要 IPC?
- 主进程和渲染进程是独立的进程,需要一种机制来传递消息和数据。
- 渲染进程需要请求主进程执行特权操作 (如读写文件、显示原生对话框)。
- 主进程需要通知渲染进程更新 UI 或传递数据。
-
主要模块:
ipcMain
(在主进程中使用)ipcRenderer
(在渲染进程或preload
脚本中使用)contextBridge
(在preload
脚本中使用,用于安全暴露 API)
-
通信模式:
- 渲染进程 -> 主进程 (单向):
- 渲染进程 (
preload
或renderer
):ipcRenderer.send(channel, ...args)
- 主进程:
ipcMain.on(channel, (event, ...args) => { ... })
- 渲染进程 (
- 渲染进程 -> 主进程 -> 渲染进程 (双向异步,请求/响应):
- 渲染进程 (
preload
或renderer
):const result = await ipcRenderer.invoke(channel, ...args)
- 主进程:
ipcMain.handle(channel, async (event, ...args) => { ...; return result; })
- 渲染进程 (
- 主进程 -> 渲染进程 (单向):
- 主进程 (需要
webContents
对象):win.webContents.send(channel, ...args)
- 渲染进程 (
preload
或renderer
):ipcRenderer.on(channel, (event, ...args) => { ... })
- 主进程 (需要
- 渲染进程 -> 主进程 (单向):
-
安全 IPC 的最佳实践 (使用
contextBridge
):main.js
: 使用ipcMain.handle
或ipcMain.on
处理来自渲染进程的请求。preload.js
:const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { // 暴露一个调用主进程函数的接口 doSomething: (data) => ipcRenderer.invoke('do-something', data), // 暴露一个监听主进程消息的接口 onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)), // 需要注意移除监听器以防内存泄漏 removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel) });
renderer.js
:// 调用暴露的函数 const result = await window.electronAPI.doSomething('some data'); console.log(result); // 监听暴露的事件 window.electronAPI.onUpdateCounter((value) => { console.log('Counter updated:', value); }); // 在组件卸载或页面关闭时清理监听器 // window.electronAPI.removeAllListeners('update-counter');
第六章:原生 UI 元素
-
应用程序菜单 (
Menu
):- 创建自定义的顶部应用程序菜单 (File, Edit, View 等)。
- 创建上下文菜单 (右键菜单)。
- 使用
Menu.buildFromTemplate(template)
创建菜单。 template
是一个包含菜单项对象的数组 (e.g.,{ label: 'File', submenu: [...] }
,{ label: 'Quit', role: 'quit' }
,{ type: 'separator' }
)。- 通过
Menu.setApplicationMenu(menu)
设置应用菜单。 - 通过
win.webContents.on('context-menu', ...)
弹出上下文菜单 (menu.popup()
)。 role
属性可以快速创建标准菜单项 (如undo
,redo
,cut
,copy
,paste
,quit
,toggledevtools
)。
-
对话框 (
dialog
):- 显示原生的系统对话框。
dialog.showOpenDialogSync() / dialog.showOpenDialog()
: 文件/文件夹选择框。dialog.showSaveDialogSync() / dialog.showSaveDialog()
: 文件保存框。dialog.showMessageBoxSync() / dialog.showMessageBox()
: 消息提示框 (info, warning, error, question)。dialog.showErrorBox()
: 显示错误信息框。- 注意:
dialog
模块只能在主进程中使用,渲染进程需要通过 IPC 调用。
-
系统托盘 (
Tray
):- 在操作系统的通知区域 (系统托盘) 创建图标。
new Tray('/path/to/icon.png')
创建实例。tray.setToolTip('Tooltip text')
设置鼠标悬停提示。tray.setContextMenu(menu)
设置右键菜单。- 监听点击事件 (
click
,right-click
等)。
-
原生通知 (
Notification
):- 显示操作系统的原生通知。
new Notification({ title: 'Title', body: 'Body text' }).show()
- 可以在主进程或支持的渲染进程中使用 (需要用户授权)。
第七章:系统集成与常用 API
-
访问文件系统 (Node.js
fs
模块):- 在主进程或通过
preload
脚本安全暴露给渲染进程。 fs.readFile()
,fs.writeFile()
,fs.mkdir()
,fs.readdir()
, etc.- 配合 Node.js
path
模块处理路径。
- 在主进程或通过
-
shell
模块:- 管理文件和外部 URL。
shell.openExternal('https://electronjs.org')
: 在默认浏览器打开链接。shell.openPath('/path/to/file')
: 用默认程序打开文件或目录。shell.showItemInFolder('/path/to/item')
: 在文件管理器中显示文件。shell.trashItem('/path/to/item')
: 将文件移动到回收站。
-
剪贴板 (
clipboard
):- 读写系统剪贴板。
clipboard.writeText('Example Text')
clipboard.readText()
clipboard.writeImage(nativeImage)
clipboard.readImage()
-
屏幕信息 (
screen
):- 获取屏幕尺寸、显示器信息、鼠标位置等。
screen.getPrimaryDisplay().workAreaSize
screen.getAllDisplays()
screen.getCursorScreenPoint()
-
系统主题 (
nativeTheme
):- 检测和响应操作系统的颜色主题 (亮色/暗色模式)。
nativeTheme.shouldUseDarkColors
(boolean)nativeTheme.on('updated', () => { ... })
监听主题变化。nativeTheme.themeSource = 'dark' / 'light' / 'system'
设置应用主题模式。
-
其他常用模块:
powerMonitor
: 监控系统电源状态 (如进入睡眠、唤醒)。globalShortcut
: 注册/注销全局键盘快捷键。protocol
: 注册自定义协议 (myapp://...
)。
第八章:安全
-
核心原则:最小权限原则
- 不要给渲染进程不必要的权限。
- 默认配置 (
nodeIntegration: false
,contextIsolation: true
) 是最安全的起点。
-
关键安全设置 (
webPreferences
):contextIsolation: true
(默认): 强烈推荐。隔离preload
脚本和渲染进程的 JavaScript 上下文,防止渲染进程直接访问 Node.js 或 Electron API。nodeIntegration: false
(默认): 强烈推荐。禁止在渲染进程中使用require()
和 Node.js 全局变量。sandbox: true
: 启用 Chromium 沙盒,进一步限制渲染进程的能力。通常需要配合contextBridge
和 IPC 使用。
-
preload
脚本的重要性:- 作为受信任的脚本,连接隔离的渲染进程和主进程。
- 使用
contextBridge.exposeInMainWorld
安全地暴露有限的、必要的 API 给渲染进程。不要暴露整个ipcRenderer
或 Node.js 模块。
-
内容安全策略 (CSP - Content Security Policy):
- 通过 HTTP Header (
session.defaultSession.webRequest.onHeadersReceived
) 或<meta>
标签设置。 - 限制资源加载来源 (脚本、样式、图片等),防止 XSS 攻击。
- 例如:
default-src 'self'
只允许加载同源资源。
- 通过 HTTP Header (
-
校验 IPC 消息:
- 不要完全信任来自渲染进程的任何数据。
- 在主进程的 IPC 处理函数中,对接收到的参数进行严格的类型、格式和范围校验。
-
限制导航:
- 监听
webContents
的will-navigate
和new-window
事件,阻止应用导航到非预期的外部网站或打开恶意窗口。
- 监听
-
检查依赖项:
- 定期更新依赖项 (
npm audit
),注意第三方库可能存在的安全漏洞。
- 定期更新依赖项 (
第九章:打包与分发
-
为什么需要打包?
- 将应用程序代码、Electron 可执行文件、Node.js 模块等捆绑成用户可以直接安装和运行的格式 (如
.exe
,.dmg
,.deb
)。 - 简化用户安装过程。
- 将应用程序代码、Electron 可执行文件、Node.js 模块等捆绑成用户可以直接安装和运行的格式 (如
-
常用打包工具:
electron-builder
: 功能强大,配置灵活,支持多种目标格式和自动更新。推荐使用。electron-packager
: 相对简单,只负责基础打包,不包含安装程序制作和自动更新。
-
electron-builder
配置 (通常在package.json
的build
字段或electron-builder.yml
文件中):appId
: 应用程序的唯一标识符 (如com.example.myapp
)。productName
: 应用名称。files
: 指定需要包含在打包中的文件/目录。directories
: 指定输出目录 (output
) 和构建资源目录 (buildResources
)。- 特定平台配置 (
win
,mac
,linux
):target
: 打包的目标格式 (e.g.,nsis
for Windows installer,dmg
for macOS,AppImage
,deb
,rpm
for Linux)。icon
: 指定应用程序图标。asar
: 是否将应用源码打包成 asar 归档文件 (提高读取性能,隐藏源码)。
-
打包命令 (使用
electron-builder
):npm run build
或yarn build
(通常配置在scripts
中,e.g.,"build": "electron-builder"
)。- 可以指定平台:
electron-builder --win --mac --linux
。
-
代码签名 (Code Signing):
- 目的: 向操作系统和用户证明应用程序来源可信,未被篡改。
- macOS: 必须进行签名和公证 (Notarization) 才能在较新系统上顺利分发。需要 Apple Developer ID 证书。
- Windows: 推荐使用 EV 证书或标准代码签名证书进行签名,以避免 SmartScreen 警告。
electron-builder
支持配置签名证书。
-
自动更新 (
electron-updater
):electron-builder
内置支持electron-updater
模块。- 需要在主进程中集成更新逻辑 (检查更新、下载、安装)。
- 配置
publish
选项 (如 GitHub Releases, S3 等) 来指定更新包的发布位置。
第十章:进阶主题与最佳实践
-
性能优化:
- 懒加载: 按需加载模块和资源,避免启动时加载所有内容。
- 优化 IPC: 避免频繁、大量数据的 IPC 通信。考虑合并请求,使用
invoke/handle
代替多次send/on
。 - 避免在渲染进程中执行阻塞操作: 将耗时任务 (如复杂计算、文件读写) 放到主进程或 Web Workers 中。
- 管理窗口: 不用的窗口及时销毁 (
win.close()
) 而不是隐藏 (win.hide()
),以释放资源。 - 使用 V8 代码缓存:
app.enableSandbox()
或通过webPreferences
控制。 - 分析性能: 使用 Chrome DevTools 的 Performance 和 Memory 面板。
-
状态管理:
- 对于复杂应用,在多个窗口/进程间同步状态可能比较复杂。
- 方案:
- 将状态主要存储在主进程,通过 IPC 同步给需要的渲染进程。
- 使用
electron-store
等库持久化简单配置。 - 使用 Redux, Vuex, Pinia 等状态管理库,并配合 IPC 或
electron-redux
,vuex-electron
等桥接库进行跨进程同步。
-
测试:
- 单元测试: 使用 Jest, Mocha 等测试框架测试独立的模块和函数 (主进程、渲染进程逻辑)。
- 端到端 (E2E) 测试: 使用 Spectron (官方维护,基于 WebDriver) 或 Playwright/Puppeteer (需要额外配置) 来模拟用户交互,测试整个应用程序的行为。
-
使用现代前端框架 (Vue, React, Angular):
- 可以将 Vue/React/Angular 项目构建后的静态文件 (
dist
目录) 加载到 Electron 的BrowserWindow
中 (win.loadFile('dist/index.html')
)。 - 通常使用 Vite 或 Webpack 等构建工具。
- 需要配置好
preload
脚本和 IPC 通信,以连接前端框架和 Electron 的原生能力。 - 社区有模板项目 (如
electron-vite
,electron-react-boilerplate
) 可以快速启动。
- 可以将 Vue/React/Angular 项目构建后的静态文件 (
-
主进程与渲染进程代码分离:
- 保持清晰的项目结构,将主进程代码、
preload
脚本、渲染进程 UI 代码分别放在不同的目录中。
- 保持清晰的项目结构,将主进程代码、
这份总结覆盖了 Electron 开发的主要方面。掌握这些知识点将为构建稳定、安全、功能丰富的桌面应用打下坚实的基础。在实践中不断深入探索和学习特定 API 及最佳实践非常重要。
示例
Electron 知识点详解 (带示例)
第一章:Electron 入门与核心概念
(本章偏重概念,代码示例从第二章开始)
-
什么是 Electron?
- 定义:使用 HTML, CSS, JavaScript 构建跨平台桌面应用的框架。
- 核心:Chromium + Node.js + 自定义 APIs。
-
为什么选择 Electron?
- 跨平台、Web 技术栈、快速开发、强大能力、成熟生态。
-
主要挑战/缺点:
- 包体积大、内存占用、潜在性能瓶颈、安全需关注。
-
核心架构:主进程 (Main Process) 与渲染进程 (Renderer Process)
- 主进程: 唯一的 Node.js 后端环境,管理窗口和系统交互。
- 渲染进程: 每个窗口的浏览器环境,负责 UI 渲染和前端逻辑。
第二章:环境搭建与基础项目
-
环境要求: Node.js, npm/yarn。
-
创建基础项目:
# 1. 创建目录并进入 mkdir my-electron-app && cd my-electron-app # 2. 初始化 npm 项目 npm init -y # 3. 安装 Electron npm install --save-dev electron # 4. 创建文件 touch main.js index.html renderer.js
-
package.json
关键配置:// package.json { "name": "my-electron-app", "version": "1.0.0", "description": "My First Electron App", "main": "main.js", // 指定主进程入口文件 "scripts": { "start": "electron .", // 定义启动命令 "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "Your Name", "license": "MIT", "devDependencies": { "electron": "^28.0.0" // 版本号可能不同 } }
-
main.js
(主进程) 基础代码:// main.js const { app, BrowserWindow } = require('electron'); const path = require('path'); function createWindow() { // 创建浏览器窗口 const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { // preload: path.join(__dirname, 'preload.js') // 预加载脚本,后续章节会用到 } }); // 加载 index.html mainWindow.loadFile('index.html'); // 打开开发者工具 (可选) mainWindow.webContents.openDevTools(); } // Electron 会在初始化后并准备 // 创建浏览器窗口时,调用这个函数。 // 部分 API 在 ready 事件触发后才能使用。 app.whenReady().then(() => { createWindow(); // 在 macOS 上,当单击 dock 图标并且没有其他窗口打开时, // 通常在应用程序中重新创建一个窗口。 app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); // 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在 // 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。 app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit(); });
-
index.html
(渲染进程) 基础代码:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> --> <title>Hello World!</title> </head> <body> <h1>Hello World!</h1> We are using Node.js <span id="node-version"></span>, Chromium <span id="chrome-version"></span>, and Electron <span id="electron-version"></span>. <script src="./renderer.js"></script> </body> </html>
-
renderer.js
(渲染进程) 基础代码 (演示访问process
对象,但依赖nodeIntegration
,后续会用更安全的方式):// renderer.js // 注意:直接访问 process 等 Node.js API 需要在 BrowserWindow 中开启 nodeIntegration: true // 这不是推荐的安全做法,后续会通过 preload 脚本实现 const information = document.getElementById('info'); const nodeVersionSpan = document.getElementById('node-version'); const chromeVersionSpan = document.getElementById('chrome-version'); const electronVersionSpan = document.getElementById('electron-version'); // 尝试获取版本信息 (如果 nodeIntegration: false, 这会报错) try { nodeVersionSpan.innerText = process.versions.node; chromeVersionSpan.innerText = process.versions.chrome; electronVersionSpan.innerText = process.versions.electron; } catch (error) { console.error("Could not access process.versions. Is nodeIntegration enabled?", error); nodeVersionSpan.innerText = 'N/A'; chromeVersionSpan.innerText = 'N/A'; electronVersionSpan.innerText = 'N/A'; }
- 重要提示: 上述
renderer.js
示例直接访问process
。为了让它工作,你需要在main.js
的BrowserWindow
配置中添加webPreferences: { nodeIntegration: true, contextIsolation: false }
。但这极不安全! 我们将在第五章展示如何使用preload
和contextBridge
安全地实现类似功能。
- 重要提示: 上述
-
启动与调试:
- 启动:
npm start
- 调试:在
main.js
中添加mainWindow.webContents.openDevTools();
后启动,即可在窗口中看到 Chrome 开发者工具。
- 启动:
第三章:主进程 (Main Process) 详解
-
app
模块:- 示例:获取应用路径
// main.js const { app } = require('electron'); console.log('User Data Path:', app.getPath('userData')); console.log('App Path:', app.getAppPath()); console.log('Is Packaged:', app.isPackaged); // 开发时为 false, 打包后为 true
- 示例:处理退出
// main.js app.on('before-quit', (event) => { console.log('App is about to quit...'); // event.preventDefault(); // 可以阻止退出 });
- 示例:获取应用路径
-
BrowserWindow
模块:- 示例:创建无边框窗口
// main.js (在 createWindow 函数内) const win = new BrowserWindow({ width: 400, height: 300, frame: false, // 移除窗口边框和标题栏 webPreferences: { /* ... */ } });
- 示例:窗口加载完成后显示 (避免白屏)
// main.js (在 createWindow 函数内) const win = new BrowserWindow({ show: false, // 先不显示 width: 800, height: 600, webPreferences: { /* ... */ } }); win.loadFile('index.html'); win.once('ready-to-show', () => { win.show(); // 页面加载好后再显示 });
- 示例:创建无边框窗口
-
webPreferences
选项 (关键配置):- 示例:配置
preload
脚本 (安全)// main.js const path = require('path'); // ... const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { // --- 安全推荐设置 --- nodeIntegration: false, // 禁用 Node.js 集成 (渲染进程) contextIsolation: true, // 开启上下文隔离 preload: path.join(__dirname, 'preload.js') // 指定预加载脚本 // -------------------- // sandbox: true, // 更严格的沙盒,需要更多 IPC 配置 } });
preload.js
的内容将在下一章展示。
- 示例:配置
第四章:渲染进程 (Renderer Process) 详解
-
角色: UI 展示与交互。
-
访问 Node.js (推荐方式:
preload
+contextBridge
) -
preload.js
脚本:- 示例:使用
contextBridge
暴露 API (安全)// preload.js const { contextBridge, ipcRenderer } = require('electron'); const os = require('os'); // preload 可以访问 Node.js 模块 contextBridge.exposeInMainWorld('electronAPI', { // 暴露一个同步获取信息的接口 (虽然不推荐同步,但可演示) getPlatform: () => os.platform(), // 暴露一个调用主进程函数的接口 (异步) setTitle: (title) => ipcRenderer.send('set-title', title), // 暴露一个双向通信的接口 (异步) openFile: () => ipcRenderer.invoke('dialog:openFile'), // 暴露一个监听主进程消息的接口 onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)), // 移除监听器的方法 removeUpdateCounterListener: () => ipcRenderer.removeAllListeners('update-counter') }); // 也可以直接在 preload 中操作 DOM,但不推荐,应由 renderer.js 负责 // window.addEventListener('DOMContentLoaded', () => { ... });
exposeInMainWorld
的第一个参数'electronAPI'
是暴露到window
对象下的键名。
- 示例:使用
-
renderer.js
(渲染进程脚本):- 示例:调用
preload
暴露的 API// renderer.js // 调用同步方法 const platformSpan = document.createElement('p'); platformSpan.textContent = `Platform: ${window.electronAPI.getPlatform()}`; document.body.appendChild(platformSpan); // 调用单向 IPC const titleButton = document.createElement('button'); titleButton.textContent = 'Set Window Title to "My App"'; titleButton.onclick = () => { window.electronAPI.setTitle('My App'); }; document.body.appendChild(titleButton); // 调用双向 IPC const openFileButton = document.createElement('button'); openFileButton.textContent = 'Open File Dialog'; openFileButton.onclick = async () => { const filePath = await window.electronAPI.openFile(); const filePathP = document.createElement('p'); filePathP.textContent = filePath ? `Selected: ${filePath}` : 'No file selected.'; document.body.appendChild(filePathP); }; document.body.appendChild(openFileButton); // 监听来自主进程的消息 const counterP = document.createElement('p'); counterP.textContent = 'Counter: 0'; document.body.appendChild(counterP); window.electronAPI.onUpdateCounter((value) => { counterP.textContent = `Counter: ${value}`; }); // 注意:在页面/组件卸载时,应调用 removeUpdateCounterListener 清理监听 // window.onbeforeunload = () => { // window.electronAPI.removeUpdateCounterListener(); // };
- 示例:调用
第五章:进程间通信 (Inter-Process Communication - IPC)
-
为什么需要 IPC? 隔离的进程间传递消息。
-
主要模块:
ipcMain
,ipcRenderer
,contextBridge
。 -
通信模式示例 (配合上一章的
preload.js
和renderer.js
)-
渲染进程 -> 主进程 (单向): (
setTitle
)renderer.js
:window.electronAPI.setTitle('New Title')
(通过 preload 调用ipcRenderer.send
)main.js
:const { app, BrowserWindow, ipcMain } = require('electron'); // ... 在 createWindow 后 ... ipcMain.on('set-title', (event, title) => { const webContents = event.sender; const win = BrowserWindow.fromWebContents(webContents); if (win) { win.setTitle(title); } });
-
渲染进程 -> 主进程 -> 渲染进程 (双向异步): (
openFile
)renderer.js
:const filePath = await window.electronAPI.openFile()
(通过 preload 调用ipcRenderer.invoke
)main.js
:const { app, BrowserWindow, ipcMain, dialog } = require('electron'); // ... ipcMain.handle('dialog:openFile', async () => { const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] }); if (!canceled && filePaths.length > 0) { return filePaths[0]; } return null; // 或者 undefined });
-
主进程 -> 渲染进程 (单向): (
update-counter
)main.js
(示例:每秒发送一次计数器):
(注意: 上述代码需要将// 需要 mainWindow 实例 let counter = 0; setInterval(() => { // 确保窗口还存在 if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-counter', counter++); } }, 1000);
mainWindow
变量提升到setInterval
可访问的作用域)renderer.js
(通过 preload 的onUpdateCounter
监听):window.electronAPI.onUpdateCounter((value) => { console.log('Received counter from main:', value); // 更新 UI... });
-
-
安全 IPC 的最佳实践: 始终使用
contextBridge
,如上例所示。避免直接暴露ipcRenderer
。
第六章:原生 UI 元素
-
应用程序菜单 (
Menu
):- 示例:创建简单的应用菜单 (macOS & Windows/Linux)
// main.js const { app, Menu, shell } = require('electron'); const isMac = process.platform === 'darwin'; const template = [ // { role: 'appMenu' } 或者 app.getName() ...(isMac ? [{ label: app.getName(), submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] }] : []), // { role: 'fileMenu' } { label: 'File', submenu: [ { label: 'New Window', accelerator: 'CmdOrCtrl+N', click: () => { /* 调用 createWindow() */ } }, isMac ? { role: 'close' } : { role: 'quit' } ] }, // { role: 'editMenu' } { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ { role: 'pasteAndMatchStyle' }, { role: 'delete' }, { role: 'selectAll' }, { type: 'separator' }, { label: 'Speech', submenu: [ { role: 'startSpeaking' }, { role: 'stopSpeaking' } ] } ] : [ { role: 'delete' }, { type: 'separator' }, { role: 'selectAll' } ]) ] }, // { role: 'viewMenu' } { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' } ] }, // { role: 'windowMenu' } { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ { type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' } ] : [ { role: 'close' } ]) ] }, { role: 'help', submenu: [ { label: 'Learn More', click: async () => { await shell.openExternal('https://electronjs.org'); } } ] } ]; const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); // 设置应用菜单 // 也可以创建上下文菜单 // const contextMenu = Menu.buildFromTemplate([...]); // window.webContents.on('context-menu', (e, params) => { // contextMenu.popup(window); // });
- 示例:创建简单的应用菜单 (macOS & Windows/Linux)
-
对话框 (
dialog
):- 示例:显示消息框 (主进程或通过 IPC 调用)
// main.js (或在 ipcMain.handle 中) const { dialog } = require('electron'); async function showInfoMessage() { await dialog.showMessageBox({ type: 'info', // 'none', 'info', 'error', 'question', 'warning' title: 'Information', message: 'This is an informational message.', detail: 'Some extra details here.', buttons: ['OK', 'Cancel'] // 返回点击按钮的索引 (0 or 1) }); } // 调用 showInfoMessage()
- 示例:显示打开文件对话框 (已在 IPC 示例中)
- 示例:显示消息框 (主进程或通过 IPC 调用)
-
系统托盘 (
Tray
):- 示例:创建简单的系统托盘图标
// main.js const { app, Tray, Menu, nativeImage } = require('electron'); const path = require('path'); let tray = null; // 需要持有引用,否则会被垃圾回收 app.whenReady().then(() => { // 需要一个图标文件 (e.g., icon.png in project root) // 推荐使用 16x16 或 32x32 的 .png 或 .ico const iconPath = path.join(__dirname, 'icon.png'); // 替换为你的图标路径 const icon = nativeImage.createFromPath(iconPath); tray = new Tray(icon); const contextMenu = Menu.buildFromTemplate([ { label: 'Show App', click: () => { /* 显示窗口逻辑 */ } }, { label: 'Quit', click: () => { app.quit(); } } ]); tray.setToolTip('My Electron App'); tray.setContextMenu(contextMenu); tray.on('click', () => { // 点击托盘图标的操作,例如显示/隐藏窗口 // mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); }); });
- 示例:创建简单的系统托盘图标
-
原生通知 (
Notification
):- 示例:显示一个简单的通知
// main.js (或在渲染进程中,但需检查支持性) const { Notification } = require('electron'); function showNotification() { if (Notification.isSupported()) { // 检查系统是否支持 const notification = new Notification({ title: 'Hello!', body: 'This is a notification from Electron.', // icon: path.join(__dirname, 'icon.png') // 可选图标 }); notification.show(); notification.on('click', () => { console.log('Notification clicked!'); // 可以添加点击后的操作,如聚焦窗口 }); } else { console.log('Notifications not supported on this system.'); } } // 调用 showNotification()
- 示例:显示一个简单的通知
第七章:系统集成与常用 API
-
访问文件系统 (Node.js
fs
模块):- 示例:通过
preload
安全暴露读取文件功能preload.js
:const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { // ... 其他 API ... readFile: (filePath) => ipcRenderer.invoke('fs:readFile', filePath) });
main.js
:const { ipcMain } = require('electron'); const fs = require('fs').promises; // 使用 promise 版本 ipcMain.handle('fs:readFile', async (event, filePath) => { try { // !! 安全警告:实际应用中必须严格校验 filePath !! // 防止路径遍历攻击等,例如限制在特定目录下 console.log(`Reading file requested by renderer: ${filePath}`); const data = await fs.readFile(filePath, 'utf-8'); return { success: true, data: data }; } catch (error) { console.error('Error reading file:', error); return { success: false, error: error.message }; } });
renderer.js
:async function readMyFile() { // 需要用户选择文件或指定安全路径 const result = await window.electronAPI.readFile('path/to/your/file.txt'); if (result.success) { console.log('File content:', result.data); } else { console.error('Failed to read file:', result.error); } }
- 示例:通过
-
shell
模块:- 示例:打开外部链接
// main.js 或 preload.js (暴露给渲染进程) const { shell } = require('electron'); // shell.openExternal('https://www.google.com'); // --- 通过 preload 暴露 --- // preload.js contextBridge.exposeInMainWorld('electronAPI', { // ... openExternal: (url) => shell.openExternal(url) // 注意安全,校验 URL }); // renderer.js // window.electronAPI.openExternal('https://electronjs.org');
- 示例:打开外部链接
-
剪贴板 (
clipboard
):- 示例:读写文本
preload.js
:const { contextBridge, clipboard } = require('electron'); contextBridge.exposeInMainWorld('clipboardAPI', { writeText: (text) => clipboard.writeText(text), readText: () => clipboard.readText() });
renderer.js
:async function testClipboard() { await window.clipboardAPI.writeText('Copied from Electron!'); const text = await window.clipboardAPI.readText(); console.log('Clipboard content:', text); } // 调用 testClipboard()
- 示例:读写文本
-
屏幕信息 (
screen
):- 示例:获取主显示器尺寸
preload.js
:const { contextBridge, screen } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { // ... getPrimaryDisplaySize: () => screen.getPrimaryDisplay().workAreaSize });
renderer.js
:const size = window.electronAPI.getPrimaryDisplaySize(); console.log(`Primary display work area: ${size.width}x${size.height}`);
- 示例:获取主显示器尺寸
-
系统主题 (
nativeTheme
):- 示例:检测并响应暗色模式
preload.js
:const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { // ... isDarkMode: () => ipcRenderer.invoke('nativeTheme:isDarkMode'), onThemeUpdate: (callback) => ipcRenderer.on('theme-updated', () => callback()) });
main.js
:const { nativeTheme, ipcMain } = require('electron'); ipcMain.handle('nativeTheme:isDarkMode', () => nativeTheme.shouldUseDarkColors); // 监听主题变化并通知渲染进程 nativeTheme.on('updated', () => { // 通知所有窗口 BrowserWindow.getAllWindows().forEach(win => { if(win && !win.isDestroyed()) { win.webContents.send('theme-updated'); } }); });
renderer.js
:
(你需要在 CSS 中定义async function checkTheme() { const isDark = await window.electronAPI.isDarkMode(); document.body.classList.toggle('dark-mode', isDark); console.log(`Current theme is ${isDark ? 'dark' : 'light'}`); } checkTheme(); // Initial check window.electronAPI.onThemeUpdate(() => { console.log('Theme updated!'); checkTheme(); // Re-check on update // 更新 UI ... });
.dark-mode
样式)
- 示例:检测并响应暗色模式
第八章:安全
-
核心原则: 最小权限。
-
关键安全设置 (
webPreferences
): 见第三章示例 (nodeIntegration: false
,contextIsolation: true
). -
preload
脚本与contextBridge
: 这是现代 Electron 安全的核心。 见第四、五章示例。永远不要在preload
中这样写:window.ipcRenderer = require('electron').ipcRenderer;
。 -
内容安全策略 (CSP):
- 示例:在 HTML 中设置
<!-- index.html --> <head> <meta charset="UTF-8"> <!-- 只允许加载同源资源 (最严格) --> <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'"> --> <!-- 允许同源脚本,允许 data: 图像,允许特定域的样式 --> <meta http-equiv="Content-Security-Policy" content="script-src 'self'; img-src 'self' data:; style-src 'self' https://trusted.cdn.com; default-src 'self'"> <title>Secure App</title> </head>
- 示例:在 HTML 中设置
-
校验 IPC 消息:
- 示例:在
ipcMain.handle
中校验// main.js ipcMain.handle('process-data', (event, input) => { // 假设 input 应该是一个包含 name 和 age 的对象 if (typeof input !== 'object' || input === null) { throw new Error('Invalid input type: expected object.'); } if (typeof input.name !== 'string' || input.name.length === 0) { throw new Error('Invalid input: name must be a non-empty string.'); } if (typeof input.age !== 'number' || input.age < 0 || input.age > 150) { throw new Error('Invalid input: age must be a number between 0 and 150.'); } // ... 处理校验通过的数据 ... console.log(`Processing valid data for ${input.name}`); return { success: true, message: `Processed ${input.name}` }; });
- 示例:在
-
限制导航:
- 示例:阻止导航到外部网站
// main.js (在 createWindow 内,获取 webContents 后) mainWindow.webContents.on('will-navigate', (event, url) => { const parsedUrl = new URL(url); // 允许 file:// 协议或特定安全域 if (parsedUrl.protocol !== 'file:' /* && parsedUrl.hostname !== 'trusted.com' */) { console.warn(`Blocked navigation to: ${url}`); event.preventDefault(); // 阻止导航 shell.openExternal(url); // 可选:在外部浏览器打开 } });
- 示例:阻止导航到外部网站
-
检查依赖项:
npm audit
第九章:打包与分发
-
为什么需要打包? 创建可执行文件。
-
常用打包工具:
electron-builder
(推荐),electron-packager
。 -
electron-builder
配置 (示例package.json
):// package.json { // ... 其他配置 ... "scripts": { "start": "electron .", "pack": "electron-builder --dir", // 打包成未压缩目录 (测试用) "dist": "electron-builder" // 打包成分发格式 (exe, dmg 等) }, "build": { "appId": "com.example.myelectronapp", "productName": "MyElectronApp", "files": [ "main.js", "preload.js", "index.html", "renderer.js", "node_modules/**/*", // 通常 builder 会自动处理 "assets/", // 包含你的静态资源 "!node_modules/**/{test,tests,spec,specs,example,examples,.bin}/**/*" // 排除不必要的文件 ], "directories": { "output": "dist", // 打包输出目录 "buildResources": "build" // 构建资源目录 (如图标) }, "win": { "target": "nsis", // NSIS 安装程序 "icon": "build/icon.ico" // Windows 图标 }, "mac": { "target": "dmg", // DMG 镜像 "icon": "build/icon.icns", // macOS 图标 "category": "public.app-category.utilities" // App Store 分类 }, "linux": { "target": [ "AppImage", "deb" ], "icon": "build/icon.png", // Linux 图标 "category": "Utility" }, "nsis": { // NSIS 安装程序特定配置 "oneClick": false, // 非静默安装 "allowToChangeInstallationDirectory": true }, "asar": true // 将应用代码打包到 asar 存档中 }, "devDependencies": { "electron": "^28.0.0", "electron-builder": "^24.9.1" // 添加 electron-builder } }
- 注意: 你需要创建
build
文件夹并放入相应格式的图标文件 (icon.ico
,icon.icns
,icon.png
)。
- 注意: 你需要创建
-
打包命令:
npm run dist
或yarn dist
-
代码签名: 需要平台特定的证书,并在
electron-builder
配置中指定 (参考其文档)。 -
自动更新 (
electron-updater
):- 示例:主进程检查更新
// main.js const { autoUpdater } = require('electron-updater'); const { dialog } = require('electron'); // 配置 autoUpdater (通常会自动读取 build.publish 配置) // autoUpdater.setFeedURL({ provider: 'github', owner: 'your-gh-username', repo: 'your-repo' }); function checkForUpdates() { // 在应用启动后或菜单项点击时调用 autoUpdater.checkForUpdatesAndNotify().catch(err => { console.error('Update check failed:', err); }); } // 监听更新事件 autoUpdater.on('update-available', () => { dialog.showMessageBox({ type: 'info', title: 'Update Available', message: 'A new version is available. Do you want to download and install it now?', buttons: ['Yes', 'Later'] }).then(result => { if (result.response === 0) { // 'Yes' button autoUpdater.downloadUpdate(); } }); }); autoUpdater.on('update-downloaded', () => { dialog.showMessageBox({ type: 'info', title: 'Update Ready', message: 'Update downloaded. The application will now quit to install...', buttons: ['OK'] }).then(() => { setImmediate(() => autoUpdater.quitAndInstall()); }); }); autoUpdater.on('error', (error) => { dialog.showErrorBox('Update Error', error == null ? "unknown" : (error.stack || error).toString()); }); // 在 app ready 后调用检查更新 app.whenReady().then(() => { // ... createWindow ... if (app.isPackaged) { // 只在打包后检查更新 checkForUpdates(); } });
- 示例:主进程检查更新
第十章:进阶主题与最佳实践
-
性能优化:
- 懒加载示例 (主进程动态 import):
// main.js ipcMain.handle('load-heavy-module', async () => { const heavyModule = await import('./heavy-module.js'); // 动态导入 return heavyModule.doWork(); });
- 避免阻塞操作: 将
fs.readFileSync
替换为fs.readFile
(异步)。
- 懒加载示例 (主进程动态 import):
-
状态管理: 使用 Redux/Vuex/Pinia 等,配合
electron-store
或自定义 IPC 同步机制。 -
测试 (Spectron E2E 示例概念):
// test/spec.js (概念性) const Application = require('spectron').Application; const assert = require('assert'); const electronPath = require('electron'); // 获取 Electron 可执行文件路径 const path = require('path'); describe('Application launch', function () { this.timeout(10000); // 增加超时 let app; beforeEach(function () { app = new Application({ path: electronPath, args: [path.join(__dirname, '..')] // 指向你的 app 根目录 }); return app.start(); }); afterEach(function () { if (app && app.isRunning()) { return app.stop(); } }); it('shows an initial window', async function () { const count = await app.client.getWindowCount(); assert.strictEqual(count, 1); }); it('should have the correct title', async function () { const title = await app.client.getTitle(); assert.strictEqual(title, 'Hello World!'); // 或你的初始标题 }); // ... 更多测试,如点击按钮、检查文本等 });
-
使用现代前端框架 (Vue/React/Angular):
- 示例:加载 Vite 构建的 Vue 应用
- 用 Vite 创建 Vue 项目:
npm create vite@latest my-vue-app --template vue-ts
- 构建 Vue 项目:
cd my-vue-app && npm install && npm run build
(会生成dist
目录) main.js
:const { app, BrowserWindow } = require('electron'); const path = require('path'); function createWindow() { const mainWindow = new BrowserWindow({ /* ... webPreferences ... */ }); if (app.isPackaged) { // 打包后加载构建的 index.html mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); // 假设 dist 目录被复制到打包后的 renderer 目录 } else { // 开发时加载 Vite 开发服务器 mainWindow.loadURL('http://localhost:5173'); // Vite 默认端口 mainWindow.webContents.openDevTools(); } } // ... app lifecycle ...
- 你需要调整打包配置 (
electron-builder
),将 Vue 构建的dist
目录包含进去,并可能调整loadFile
的路径。使用electron-vite
模板可以简化这个过程。
- 用 Vite 创建 Vue 项目:
- 示例:加载 Vite 构建的 Vue 应用
-
主进程与渲染进程代码分离:
- 项目结构示例:
my-electron-app/ ├── build/ # 图标等构建资源 ├── dist/ # electron-builder 输出目录 ├── node_modules/ ├── src/ │ ├── main/ # 主进程代码 │ │ ├── main.js # 主入口 │ │ └── modules/ # 主进程其他模块 │ ├── preload/ # Preload 脚本 │ │ └── preload.js │ └── renderer/ # 渲染进程代码 (UI) │ ├── index.html │ ├── renderer.js │ └── style.css ├── package.json └── ... 其他配置文件 ...
- 项目结构示例:
这些示例应该能让你更具体地理解 Electron 的各个核心概念和常用功能。记住,安全和性能是 Electron 开发中需要持续关注的重要方面。
更多推荐
所有评论(0)