【前端】【Electron】Electron 知识点详解,看着一篇文章就够了
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 结构。
- 可以通过 引入渲染进程的 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 (渲染进程脚本):
- 通过 标签在 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) 或 标签设置。
- 限制资源加载来源 (脚本、样式、图片等),防止 XSS 攻击。
- 例如:default-src 'self' 只允许加载同源资源。
-
校验 IPC 消息:
- 不要完全信任来自渲染进程的任何数据。
- 在主进程的 IPC 处理函数中,对接收到的参数进行严格的类型、格式和范围校验。
-
限制导航:
- 监听 webContents 的 will-navigate 和 new-window 事件,阻止应用导航到非预期的外部网站或打开恶意窗口。
-
检查依赖项:
- 定期更新依赖项 (npm audit),注意第三方库可能存在的安全漏洞。
第九章:打包与分发
-
为什么需要打包?
- 将应用程序代码、Electron 可执行文件、Node.js 模块等捆绑成用户可以直接安装和运行的格式 (如 .exe, .dmg, .deb)。
- 简化用户安装过程。
-
常用打包工具:
- 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) 可以快速启动。
-
主进程与渲染进程代码分离:
- 保持清晰的项目结构,将主进程代码、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 (渲染进程) 基础代码:
Hello World!
Hello World!
We are using Node.js , Chromium , and Electron . -
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 的内容将在下一章展示。
- 示例:配置 preload 脚本 (安全)
- 示例:创建无边框窗口
- 示例:获取应用路径
第四章:渲染进程 (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(); // };
- 示例:调用 preload 暴露的 API
- 示例:使用 contextBridge 暴露 API (安全)
第五章:进程间通信 (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。
- main.js (示例:每秒发送一次计数器):
-
第六章:原生 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); // });
-
对话框 (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 示例中)
-
系统托盘 (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()
- 示例:显示一个简单的通知
- 示例:创建简单的系统托盘图标
- 示例:显示消息框 (主进程或通过 IPC 调用)
- 示例:创建简单的应用菜单 (macOS & Windows/Linux)
第七章:系统集成与常用 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:
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 ... });
(你需要在 CSS 中定义 .dark-mode 样式)
- preload.js:
- 示例:检测并响应暗色模式
- preload.js:
- 示例:获取主显示器尺寸
- preload.js:
- 示例:读写文本
- 示例:打开外部链接
- preload.js:
- 示例:通过 preload 安全暴露读取文件功能
第八章:安全
-
核心原则: 最小权限。
-
关键安全设置 (webPreferences): 见第三章示例 (nodeIntegration: false, contextIsolation: true).
-
preload 脚本与 contextBridge: 这是现代 Electron 安全的核心。 见第四、五章示例。永远不要在 preload 中这样写:window.ipcRenderer = require('electron').ipcRenderer;。
-
内容安全策略 (CSP):
- 示例:在 HTML 中设置
Secure App
-
校验 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 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
- 示例:阻止导航到外部网站
- 示例:在 ipcMain.handle 中校验
- 示例:在 HTML 中设置
第九章:打包与分发
-
为什么需要打包? 创建可执行文件。
-
常用打包工具: 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 (异步)。
-
状态管理: 使用 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 模板可以简化这个过程。
-
主进程与渲染进程代码分离:
- 项目结构示例:
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 └── ... 其他配置文件 ...
- 项目结构示例:
- 示例:加载 Vite 构建的 Vue 应用
- 懒加载示例 (主进程动态 import):
这些示例应该能让你更具体地理解 Electron 的各个核心概念和常用功能。记住,安全和性能是 Electron 开发中需要持续关注的重要方面。