Electron 知识点详解
第一章:Electron 入门与核心概念
什么是 Electron?
为什么选择 Electron?
Electron 的主要挑战/缺点:
核心架构:主进程 (Main Process) 与渲染进程 (Renderer Process)
main.js
或 package.json
中指定的入口文件)。BrowserWindow
(渲染进程)、处理原生操作系统交互 (菜单、对话框、托盘等)。BrowserWindow
实例拥有一个独立的渲染进程。第二章:环境搭建与基础项目
环境要求:
创建基础项目:
mkdir my-electron-app && cd my-electron-app
npm init -y
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()
。win.loadFile('index.html')
或 win.loadURL('http://localhost:3000')
(用于加载开发服务器)。window-all-closed
, activate
)。index.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) 详解
角色:
访问 Node.js (不推荐直接开启 nodeIntegration
):
nodeIntegration: true
,渲染进程中的任何脚本 (包括第三方库) 都可以访问文件系统、执行命令等,容易受到 XSS 攻击影响。preload
脚本 + contextBridge
。preload.js
脚本:
webPreferences
中通过 preload
选项指定。contextIsolation: true
)。window
和 document
对象。contextBridge.exposeInMainWorld(apiKey, apiObject)
安全地向渲染进程暴露选择性的 Node.js 功能或 IPC 调用接口。renderer.js
(渲染进程脚本):
标签在 HTML 中引入。preload
脚本暴露的 API。contextBridge
,可以通过 window[apiKey]
访问暴露的接口。第五章:进程间通信 (Inter-Process Communication - IPC)
为什么需要 IPC?
主要模块:
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
):
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.path
模块处理路径。shell
模块:
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):
session.defaultSession.webRequest.onHeadersReceived
) 或
标签设置。default-src 'self'
只允许加载同源资源。校验 IPC 消息:
限制导航:
webContents
的 will-navigate
和 new-window
事件,阻止应用导航到非预期的外部网站或打开恶意窗口。检查依赖项:
npm audit
),注意第三方库可能存在的安全漏洞。第九章:打包与分发
为什么需要打包?
.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):
electron-builder
支持配置签名证书。自动更新 (electron-updater
):
electron-builder
内置支持 electron-updater
模块。publish
选项 (如 GitHub Releases, S3 等) 来指定更新包的发布位置。第十章:进阶主题与最佳实践
性能优化:
invoke/handle
代替多次 send/on
。win.close()
) 而不是隐藏 (win.hide()
),以释放资源。app.enableSandbox()
或通过 webPreferences
控制。状态管理:
electron-store
等库持久化简单配置。electron-redux
, vuex-electron
等桥接库进行跨进程同步。测试:
使用现代前端框架 (Vue, React, Angular):
dist
目录) 加载到 Electron 的 BrowserWindow
中 (win.loadFile('dist/index.html')
)。preload
脚本和 IPC 通信,以连接前端框架和 Electron 的原生能力。electron-vite
, electron-react-boilerplate
) 可以快速启动。主进程与渲染进程代码分离:
preload
脚本、渲染进程 UI 代码分别放在不同的目录中。这份总结覆盖了 Electron 开发的主要方面。掌握这些知识点将为构建稳定、安全、功能丰富的桌面应用打下坚实的基础。在实践中不断深入探索和学习特定 API 及最佳实践非常重要。
Electron 知识点详解 (带示例)
第一章:Electron 入门与核心概念
(本章偏重概念,代码示例从第二章开始)
什么是 Electron?
为什么选择 Electron?
主要挑战/缺点:
核心架构:主进程 (Main Process) 与渲染进程 (Renderer Process)
第二章:环境搭建与基础项目
环境要求: 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">
<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
):
// 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
):
// 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()
系统托盘 (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
: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
样式)第八章:安全
核心原则: 最小权限。
关键安全设置 (webPreferences
): 见第三章示例 (nodeIntegration: false
, contextIsolation: true
).
preload
脚本与 contextBridge
: 这是现代 Electron 安全的核心。 见第四、五章示例。永远不要在 preload
中这样写:window.ipcRenderer = require('electron').ipcRenderer;
。
内容安全策略 (CSP):
<head>
<meta charset="UTF-8">
<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 Apptitle>
head>
校验 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();
}
});
第十章:进阶主题与最佳实践
性能优化:
// 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):
npm create vite@latest my-vue-app --template vue-ts
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
└── ... 其他配置文件 ...
这些示例应该能让你更具体地理解 Electron 的各个核心概念和常用功能。记住,安全和性能是 Electron 开发中需要持续关注的重要方面。