一、Electron 概述与环境搭建
1.1 什么是 Electron
Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的开源框架。它通过将 Chromium 浏览器引擎和 Node.js 运行时嵌入到二进制文件中,允许开发者使用单一的 JavaScript 代码库创建能在 Windows、macOS 和 Linux 上运行的应用程序,无需原生开发经验。简单来说,Electron 让 Web 开发者能够利用他们已有的前端技能来开发桌面应用。
Electron 由 GitHub 开发,最初名为 Atom Shell,现在是 OpenJS 基金会的一个项目,由活跃的社区贡献者维护。它的核心优势在于一次编写,处处运行的跨平台特性,这使得开发者可以用熟悉的技术栈创建桌面应用,而无需为每个操作系统单独编写代码。
1.2 为什么选择 Electron
作为面向小白的 Electron 教程,我们需要明确为什么 Electron 是一个值得学习的框架:
1.3 环境搭建
在开始编写第一个 Electron 应用之前,我们需要搭建必要的开发环境。
1.3.1 安装 Node.js
Electron 基于 Node.js,因此首先需要安装 Node.js。Node.js 的安装程序会自动安装 npm(Node.js 包管理器),这是我们安装 Electron 和其他依赖的主要工具。
安装步骤:
node -v
npm -v
如果安装成功,这两个命令会分别输出版本号。本书假设你已经安装了 Node.js 20 或更高版本,因为 Electron 在 2025 年初已经将其 npm 生态系统迁移到 Node.js 22 作为最低支持版本。
1.3.2 安装 Electron
安装 Electron 有两种主要方式:全局安装和本地安装。对于开发应用,建议本地安装,这样可以更好地控制版本并避免权限问题。
本地安装步骤:
mkdir my-electron-app
cd my-electron-app
npm init -y
这会在项目目录中生成一个package.json文件,用于管理项目依赖和配置。
npm install electron –save-dev
全局安装(可选):
如果你想在命令行中直接使用 Electron 命令,可以全局安装 Electron:
npm install electron -g
1.3.3 验证 Electron 安装
安装完成后,可以通过以下步骤验证 Electron 是否正确安装:
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello Electron!</title>
</head>
<body>
<h1>Hello Electron!</h1>
<p>这是你的第一个Electron应用。</p>
</body>
</html>
"scripts": {
"start": "electron ."
}
npm start
如果一切顺利,你应该会看到一个显示 "Hello Electron!" 的窗口,这表示你的 Electron 环境已经搭建成功。
1.4 Electron 应用的基本结构
在深入学习 Electron 的 API 之前,了解 Electron 应用的基本结构是非常重要的。Electron 应用主要由两部分组成:主进程和渲染进程。
1.4.1 主进程
主进程是 Electron 应用的入口点,它负责创建和管理应用窗口。在主进程中,你可以使用 Electron 提供的各种模块来控制应用的生命周期、创建窗口、处理系统事件等。
主进程的特点:
- 主进程文件(通常是main.js)在应用启动时被执行。
- 使用app模块管理应用的生命周期。
- 使用BrowserWindow模块创建和管理应用窗口。
- 可以访问 Node.js 的所有 API。
- 主进程中的每个BrowserWindow实例都会在渲染进程中创建一个对应的网页。
1.4.2 渲染进程
渲染进程负责显示用户界面,每个BrowserWindow实例都会运行在自己的渲染进程中。渲染进程本质上是一个运行在 Chromium 浏览器中的网页,可以使用标准的 Web 技术(HTML、CSS、JavaScript)来构建用户界面。
渲染进程的特点:
- 每个渲染进程都是独立的,它们之间不能直接通信。
- 默认情况下,渲染进程中的 JavaScript 无法直接访问 Node.js 的 API。
- 可以通过ipcRenderer和ipcMain模块与主进程进行通信。
- 可以使用标准的 Web APIs,如 DOM 操作、fetch 等。
1.4.3 进程间通信
由于主进程和渲染进程是相互独立的,它们之间的通信需要通过 Electron 提供的 IPC(Inter-Process Communication)机制。
主要的 IPC 模块有:
- ipcMain:用于在主进程中监听和发送消息。
- ipcRenderer:用于在渲染进程中监听和发送消息。
- remote:提供了一种在渲染进程中直接调用主进程模块的方法,但不推荐在生产环境中使用,因为它可能导致安全问题。
二、主进程 API 详解
2.1 app 模块:应用生命周期管理
app模块是 Electron 中最重要的模块之一,它负责管理应用的生命周期和全局设置。在主进程中,你可以通过const { app } = require('electron')来获取app模块的实例。
2.1.1 应用生命周期事件
app模块提供了一系列事件,用于监听应用的不同生命周期阶段:
app.whenReady()方法
app.whenReady()返回一个 Promise,当 Electron 完成初始化并准备好创建浏览器窗口时,这个 Promise 会被 resolve。这是在主进程中创建窗口的最佳时机。
示例:
const { app, BrowserWindow } = require('electron');
app.whenReady().then(() => {
createWindow();
});
app.on('window-all-closed', callback)事件
当所有窗口都被关闭时触发。在这个事件中,你可以决定是否退出应用。
示例:
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { // 在macOS上,用户通常通过Cmd+Q显式退出
app.quit();
}
});
app.on('activate', callback)事件
当应用被激活时触发,通常发生在点击应用图标或从 dock 中选择应用时。在 macOS 上,即使所有窗口都关闭了,应用仍然处于激活状态,这时需要重新创建窗口。
示例:
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on('before-quit', callback)事件
在应用即将退出前触发。你可以在这个事件中执行一些清理工作,如保存数据或关闭文件句柄。
示例:
app.on('before-quit', (event) => {
// 执行清理工作
console.log('应用即将退出');
});
2.1.2 应用信息相关方法
app模块还提供了一些获取应用信息的方法:
app.getVersion()方法
返回应用的版本号。如果在package.json中找不到版本号,就返回当前包或可执行文件的版本。
示例:
console.log(`应用版本:${app.getVersion()}`);
app.getName()方法
返回应用的名称,默认为package.json中的productName字段的值,如果没有设置,则为package.json中的name字段的值。
示例:
console.log(`应用名称:${app.getName()}`);
app.getPath(name)方法
返回应用相关的特殊路径,如用户数据目录、缓存目录等。name参数可以是以下值之一:'userData', 'appData', 'cache', 'temp', 'exe', 'home', 'desktop', 'documents', 'downloads', 'music', 'pictures', 'videos'。
示例:
console.log(`用户数据目录:${app.getPath('userData')}`);
console.log(`缓存目录:${app.getPath('cache')}`);
2.1.3 应用设置方法
app模块还提供了一些设置应用行为的方法:
app.setAppUserModelId(id)方法
在 Windows 上设置应用的用户模型 ID,这会影响任务栏图标和用户体验。这个 ID 应该是一个唯一的字符串,通常采用反向域名格式。
示例:
app.setAppUserModelId('com.example.myapp');
app.setLoginItemSettings(settings)方法
设置应用是否在系统启动时自动运行。settings是一个对象,可以包含以下属性:openAtLogin(是否在登录时打开)、openAsHidden(是否以隐藏方式打开)、path(要运行的路径,默认为当前应用)、args(要传递的参数数组)。
示例:
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: false
});
2.2 BrowserWindow 模块:创建和管理窗口
BrowserWindow模块用于创建和管理应用窗口,是主进程中另一个核心模块。你可以通过const { BrowserWindow } = require('electron')来获取BrowserWindow类。
2.2.1 创建窗口
要创建一个窗口,首先需要实例化BrowserWindow类:
new BrowserWindow([options])构造函数
options参数是一个可选的对象,用于设置窗口的各种属性。以下是一些常用的窗口属性:
- width:窗口宽度,默认值为 800 像素。
- height:窗口高度,默认值为 600 像素。
- minWidth:窗口最小宽度。
- minHeight:窗口最小高度。
- maxWidth:窗口最大宽度。
- maxHeight:窗口最大高度。
- x:窗口初始 x 坐标。
- y:窗口初始 y 坐标。
- title:窗口标题。
- icon:窗口图标路径。
- webPreferences:网页偏好设置,如是否启用 Node.js 集成等。
示例:创建一个基本窗口
const win = new BrowserWindow({
width: 1024,
height: 768,
minWidth: 800,
minHeight: 600,
title: 'My Electron App',
webPreferences: {
nodeIntegration: true // 是否在渲染进程中启用Node.js集成
}
});
2.2.2 窗口加载内容
创建窗口后,需要为其加载内容。可以通过以下方法加载不同类型的内容:
win.loadFile(filePath)方法
加载本地 HTML 文件。filePath是相对于应用根目录的路径。
示例:
win.loadFile('index.html');
win.loadURL(url)方法
加载远程 URL 或本地文件。对于本地文件,需要使用file://协议。
示例:
// 加载远程URL
win.loadURL('https://www.electronjs.org');
// 加载本地文件
win.loadURL('file://' + __dirname + '/index.html');
win.loadURL(dataUrl)方法
直接加载 Data URL 格式的内容。
示例:
win.loadURL('data:text/html,<h1>Hello World!</h1>');
2.2.3 窗口操作方法
BrowserWindow类提供了一系列方法来操作窗口:
窗口状态控制
- win.show():显示窗口。
- win.hide():隐藏窗口。
- win.toggleVisibility():切换窗口可见性。
- win.focus():聚焦窗口。
- win.blur():使窗口失去焦点。
- win.minimize():最小化窗口。
- win.maximize():最大化窗口。
- win.unmaximize():取消最大化窗口。
- win.restore():恢复窗口到正常大小。
- win.isMinimized():检查窗口是否最小化。
- win.isMaximized():检查窗口是否最大化。
- win.isFocused():检查窗口是否获得焦点。
窗口尺寸和位置
- win.setPosition(x, y[, animate]):设置窗口位置。animate参数表示是否平滑过渡。
- win.getPosition():获取窗口位置,返回一个数组[x, y]。
- win.setSize(width, height[, animate]):设置窗口大小。
- win.getSize():获取窗口大小,返回一个数组[width, height]。
- win.setMinimumSize(width, height):设置窗口最小尺寸。
- win.setMaximumSize(width, height):设置窗口最大尺寸。
- win.setResizable(resizable):设置窗口是否可调整大小。
- win.isResizable():检查窗口是否可调整大小。
示例:窗口操作综合示例
// 窗口加载完成后执行一些操作
win.once('ready-to-show', () => {
// 显示窗口并聚焦
win.show();
win.focus();
// 设置窗口位置和大小
win.setPosition(100, 100);
win.setSize(1200, 800);
// 设置最小尺寸
win.setMinimumSize(800, 600);
// 设置不可调整大小
win.setResizable(false);
});
2.2.4 窗口事件
BrowserWindow类触发多个事件,可以通过on和once方法来监听这些事件:
窗口生命周期事件
- 'ready-to-show':当窗口内容准备好显示时触发。
- 'closed':当窗口被关闭时触发。
- 'close':当窗口即将关闭时触发。可以通过event.preventDefault()阻止窗口关闭。
- 'destroy':当窗口对象被销毁时触发。
窗口状态变化事件
- 'focus':当窗口获得焦点时触发。
- 'blur':当窗口失去焦点时触发。
- 'maximize':当窗口最大化时触发。
- 'unmaximize':当窗口取消最大化时触发。
- 'minimize':当窗口最小化时触发。
- 'restore':当窗口从最小化或最大化状态恢复时触发。
- 'move':当窗口位置改变时触发。
- 'resize':当窗口大小改变时触发。
示例:窗口事件监听示例
// 监听窗口关闭事件
win.on('close', (event) => {
// 可以在这里询问用户是否保存数据
const answer = confirm('是否要关闭应用?');
if (!answer) {
event.preventDefault(); // 阻止窗口关闭
}
});
// 监听窗口最大化事件
win.on('maximize', () => {
console.log('窗口已最大化');
});
// 监听窗口恢复事件
win.on('restore', () => {
console.log('窗口已恢复');
});
// 监听窗口移动事件
win.on('move', () => {
console.log(`窗口新位置:${win.getPosition()}`);
});
2.2.5 多窗口管理
在 Electron 应用中,通常需要管理多个窗口。BrowserWindow类提供了一些静态方法来管理所有窗口:
BrowserWindow.getAllWindows()方法
返回所有BrowserWindow实例的数组。
示例:
const windows = BrowserWindow.getAllWindows();
console.log(`当前打开的窗口数量:${windows.length}`);
BrowserWindow.fromId(id)方法
根据窗口 ID 获取BrowserWindow实例。窗口 ID 是一个整数,每个窗口都有唯一的 ID。
示例:
const win = BrowserWindow.fromId(1);
if (win) {
win.focus();
}
BrowserWindow.getFocusedWindow()方法
获取当前获得焦点的窗口实例,如果没有则返回null。
示例:
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
console.log(`当前聚焦的窗口标题:${focusedWindow.getTitle()}`);
}
2.3 Menu 模块:创建应用菜单
在桌面应用中,菜单是用户交互的重要组成部分。Electron 提供了Menu模块来创建和管理应用菜单。
2.3.1 创建菜单
要创建菜单,首先需要定义菜单模板。菜单模板是一个数组,其中每个元素代表一个菜单项或子菜单。
菜单项属性
每个菜单项可以包含以下属性:
- label:菜单项的文本标签。
- accelerator:快捷键,如CmdOrCtrl+O。
- icon:菜单项的图标路径。
- type:菜单项的类型,可以是'normal', 'separator', 'submenu', 'checkbox', 'radio'。
- submenu:子菜单,是一个菜单项数组。
- click:点击菜单项时执行的回调函数。
- enabled:是否启用菜单项,默认值为true。
- visible:是否显示菜单项,默认值为true。
- checked:对于复选框和单选按钮类型的菜单项,是否被选中。
示例:基本菜单模板
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => { /* 处理新建文件 */ }
},
{
label: '打开…',
accelerator: 'CmdOrCtrl+O',
click: () => { /* 处理打开文件 */ }
},
{
type: 'separator'
},
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => { app.quit(); }
}
]
},
{
label: '编辑',
submenu: [
{
label: '剪切',
accelerator: 'CmdOrCtrl+X',
click: () => { /* 处理剪切 */ }
},
{
label: '复制',
accelerator: 'CmdOrCtrl+C',
click: () => { /* 处理复制 */ }
},
{
label: '粘贴',
accelerator: 'CmdOrCtrl+V',
click: () => { /* 处理粘贴 */ }
}
]
}
];
创建并应用菜单
定义好菜单模板后,可以使用Menu.buildFromTemplate(template)方法创建菜单对象,然后使用Menu.setApplicationMenu(menu)方法将其设置为应用菜单。
示例:
const { Menu } = require('electron');
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
2.3.2 上下文菜单
除了应用菜单外,Electron 还支持上下文菜单(右键菜单)。创建上下文菜单的方法与应用菜单类似,但通常是在需要时动态创建和显示。
示例:创建并显示上下文菜单
const { Menu } = require('electron');
// 定义上下文菜单模板
const contextMenuTemplate = [
{
label: '复制',
accelerator: 'CmdOrCtrl+C',
click: () => { /* 处理复制 */ }
},
{
label: '粘贴',
accelerator: 'CmdOrCtrl+V',
click: () => { /* 处理粘贴 */ }
}
];
// 在渲染进程中监听右键点击事件
const { ipcRenderer } = require('electron');
document.addEventListener('contextmenu', (event) => {
event.preventDefault(); // 阻止默认的上下文菜单
ipcRenderer.send('show-context-menu'); // 发送消息给主进程
});
// 在主进程中监听消息并显示上下文菜单
ipcMain.on('show-context-menu', (event) => {
const menu = Menu.buildFromTemplate(contextMenuTemplate);
menu.popup({ window: BrowserWindow.getFocusedWindow() }); // 在当前聚焦窗口中显示菜单
});
2.3.3 系统默认菜单
Electron 提供了一些系统默认的菜单项和行为,特别是在 macOS 上,应用菜单通常遵循系统标准。
macOS 特殊处理
在 macOS 上,应用菜单通常包含应用名称、编辑菜单、窗口管理菜单等。Electron 会自动为这些标准菜单添加一些默认行为,但你也可以自定义它们。
示例:macOS 专用菜单项
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{
label: '关于 ' + app.getName(),
role: 'about'
},
{
type: 'separator'
},
{
label: '退出',
accelerator: 'Cmd+Q',
click: () => { app.quit(); }
}
]
});
// 编辑菜单
const editMenu = template.find(item => item.label === '编辑');
editMenu.submenu.push(
{ type: 'separator' },
{
label: '全选',
accelerator: 'Cmd+A',
click: () => { /* 处理全选 */ }
}
);
// 窗口菜单
template.push({
label: '窗口',
role: 'window',
submenu: [
{
label: '最小化',
accelerator: 'Cmd+M',
role: 'minimize'
},
{
label: '关闭',
accelerator: 'Cmd+W',
role: 'close'
},
{ type: 'separator' },
{
label: '全部显示',
role: 'front'
}
]
});
}
2.4 Tray 模块:系统托盘图标
在许多桌面应用中,系统托盘(也称为任务栏或 dock)图标是一个常见的功能。Electron 提供了Tray模块来创建和管理系统托盘图标。
2.4.1 创建系统托盘
要创建系统托盘,首先需要实例化Tray类,并传入图标路径。
new Tray(imagePath)构造函数
imagePath参数是图标的路径,可以是相对路径或绝对路径。在 macOS 上,图标应该是一个.icns 文件;在 Windows 和 Linux 上,可以是.ico 或.png 文件。
示例:
const { Tray } = require('electron');
const tray = new Tray(path.join(__dirname, 'icon.png'));
2.4.2 设置工具提示和菜单
创建系统托盘后,可以设置工具提示和上下文菜单。
tray.setToolTip(tooltip)方法
设置鼠标悬停时显示的工具提示。
示例:
tray.setToolTip('我的Electron应用');
tray.setContextMenu(menu)方法
设置右键点击托盘图标时显示的上下文菜单。
示例:
const contextMenu = Menu.buildFromTemplate([
{
label: '打开应用',
click: () => { mainWindow.show(); }
},
{
label: '退出',
click: () => { app.quit(); }
}
]);
tray.setContextMenu(contextMenu);
2.4.3 托盘事件
Tray类触发多个事件,可以通过on和once方法来监听这些事件。
'click'事件
当托盘图标被点击时触发,回调函数接收event和bounds参数。bounds是一个对象,包含托盘图标的位置和大小。
示例:
tray.on('click', (event, bounds) => {
// 点击托盘图标时切换窗口显示状态
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
});
'double-click'事件
当托盘图标被双击时触发,回调参数与'click'事件相同。
示例:
tray.on('double-click', () => {
mainWindow.maximize();
});
'right-click'事件
当托盘图标被右键点击时触发,回调参数与'click'事件相同。
示例:
tray.on('right-click', () => {
// 显示上下文菜单
contextMenu.popup({ window: mainWindow });
});
2.5 Dialog 模块:系统对话框
系统对话框是桌面应用中与用户交互的重要方式。Electron 提供了Dialog模块来创建各种类型的系统对话框。
2.5.1 消息对话框
消息对话框用于向用户显示信息、警告或错误消息,并可以获取用户的反馈。
dialog.showMessageBox([browserWindow, ]options)方法
显示一个消息对话框。browserWindow参数是可选的,指定对话框所属的窗口;options参数是一个对象,包含以下属性:
- type:对话框类型,可以是'none', 'info', 'error', 'question', 'warning'。
- title:对话框标题。
- message:对话框的主要消息。
- detail:对话框的详细内容。
- buttons:按钮文本数组,默认为['确定']。
- defaultId:默认选中的按钮索引。
- cancelId:取消按钮的索引。
- icon:对话框图标路径。
- noLink:是否隐藏链接,仅在 macOS 上有效。
该方法返回一个 Promise,resolve 的值是用户点击的按钮索引。
示例:基本消息对话框
const { dialog } = require('electron');
dialog.showMessageBox({
type: 'question',
title: '确认退出',
message: '你确定要退出应用吗?',
buttons: ['取消', '确定']
}).then(result => {
if (result.response === 1) {
app.quit();
}
});
2.5.2 文件选择对话框
文件选择对话框允许用户选择文件或目录。
dialog.showOpenDialog([browserWindow, ]options)方法
显示一个打开文件对话框。options参数是一个对象,包含以下属性:
- title:对话框标题。
- defaultPath:默认路径。
- buttonLabel:按钮标签。
- filters:文件过滤器,是一个数组,每个元素包含name和extensions属性。
- properties:对话框属性数组,可以包含'openFile', 'openDirectory', 'multiSelections', 'showHiddenFiles', 'createDirectory', 'promptToCreate', 'noResolveAliases', 'dontAddToRecent'。
该方法返回一个 Promise,resolve 的值是一个对象,包含filePaths属性(选中的文件路径数组)。
示例:打开文件对话框
const { dialog } = require('electron');
dialog.showOpenDialog({
title: '打开文件',
defaultPath: app.getPath('documents'),
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['openFile', 'multiSelections']
}).then(result => {
if (!result.canceled) {
console.log('选中的文件:', result.filePaths);
}
});
dialog.showSaveDialog([browserWindow, ]options)方法
显示一个保存文件对话框。options参数与showOpenDialog类似,还可以包含defaultPath属性指定默认文件名。
示例:保存文件对话框
dialog.showSaveDialog({
title: '保存文件',
defaultPath: path.join(app.getPath('documents'), 'new-file.txt'),
filters: [
{ name: '文本文件', extensions: ['txt'] }
]
}).then(result => {
if (!result.canceled) {
console.log('保存路径:', result.filePath);
}
});
2.5.3 其他类型对话框
除了消息对话框和文件选择对话框外,Dialog模块还提供了其他类型的对话框:
dialog.showErrorBox(title, content)方法
显示一个错误消息框,是showMessageBox的简化版,专门用于显示错误信息。
示例:
dialog.showErrorBox('错误', '无法打开文件,请检查文件路径是否正确。');
dialog.showCertificateTrustDialog([browserWindow, ]options)方法
显示证书信任对话框,用于处理 SSL 证书问题。
dialog.showMessageBoxSync([browserWindow, ]options)方法
同步版本的showMessageBox,会阻塞当前进程直到对话框关闭。不建议在主进程中使用,因为会阻塞事件循环。
三、渲染进程 API 详解
3.1 ipcRenderer 模块:渲染进程通信
在 Electron 中,渲染进程和主进程之间需要进行通信,ipcRenderer模块提供了在渲染进程中与主进程通信的能力。你可以通过const { ipcRenderer } = require('electron')来获取ipcRenderer模块的实例。
3.1.1 发送消息到主进程
渲染进程可以使用ipcRenderer.send(channel[, …args])方法向主进程发送消息。
示例:
// 发送简单消息
ipcRenderer.send('message-from-renderer', '你好,主进程!');
// 发送包含数据的消息
ipcRenderer.send('data-from-renderer', { key: 'value', array: [1, 2, 3] });
3.1.2 接收主进程消息
渲染进程可以使用ipcRenderer.on(channel, listener)方法监听来自主进程的消息。
示例:
ipcRenderer.on('message-from-main', (event, message) => {
console.log('收到主进程消息:', message);
});
ipcRenderer.on('data-from-main', (event, data) => {
console.log('收到主进程数据:', data);
});
3.1.3 发送同步消息
在某些情况下,渲染进程需要发送同步消息并立即获取响应。可以使用ipcRenderer.sendSync(channel[, …args])方法。
示例:
// 发送同步消息并获取响应
const response = ipcRenderer.sendSync('sync-message', '请求数据');
console.log('主进程响应:', response);
** 注意:** 同步消息会阻塞渲染进程,可能导致应用卡顿,应谨慎使用。
3.1.4 一次性监听
如果只需要监听一次来自主进程的消息,可以使用ipcRenderer.once(channel, listener)方法。
示例:
ipcRenderer.once('one-time-message', (event, message) => {
console.log('一次性消息:', message);
});
3.2 remote 模块:直接访问主进程对象
remote模块允许渲染进程直接访问主进程中的对象,而无需通过ipcRenderer和ipcMain进行显式通信。不过,由于安全和性能考虑,不建议在生产环境中使用remote模块。
3.2.1 使用 remote 模块
要使用remote模块,可以通过const { remote } = require('electron')来获取。
示例:获取主进程中的 BrowserWindow 对象
const { remote } = require('electron');
const BrowserWindow = remote.BrowserWindow;
// 获取当前窗口的引用
const currentWindow = BrowserWindow.getFocusedWindow();
console.log(`当前窗口标题:${currentWindow.getTitle()}`);
3.2.2 remote 模块的替代方案
由于remote模块存在安全风险,推荐使用ipcRenderer和ipcMain进行进程间通信。以下是一个替代方案的示例:
渲染进程代码:
const { ipcRenderer } = require('electron');
// 发送消息请求窗口标题
ipcRenderer.send('request-window-title');
// 监听响应
ipcRenderer.on('window-title', (event, title) => {
console.log('窗口标题:', title);
});
主进程代码:
const { ipcMain } = require('electron');
ipcMain.on('request-window-title', (event) => {
const window = BrowserWindow.getFocusedWindow();
event.sender.send('window-title', window.getTitle());
});
3.3 webFrame 模块:操作网页内容
webFrame模块允许在渲染进程中操作当前网页的框架(frame),包括缩放、执行 JavaScript 代码等。
3.3.1 网页缩放
webFrame模块提供了一些方法来控制网页的缩放级别。
webFrame.setZoomFactor(factor)方法
设置网页的缩放因子。factor是一个数值,1.0 表示正常大小,2.0 表示双倍大小,0.5 表示一半大小。
示例:
const { webFrame } = require('electron');
// 放大50%
webFrame.setZoomFactor(1.5);
// 缩小25%
webFrame.setZoomFactor(0.75);
webFrame.getZoomFactor()方法
获取当前的缩放因子。
示例:
console.log(`当前缩放因子:${webFrame.getZoomFactor()}`);
webFrame.setZoomLevel(level)方法
设置网页的缩放级别。level是一个整数,0 表示正常大小,正数表示放大,负数表示缩小。
示例:
// 放大一级
webFrame.setZoomLevel(1);
// 缩小两级
webFrame.setZoomLevel(-2);
3.3.2 执行 JavaScript 代码
webFrame模块允许在当前网页的上下文中执行 JavaScript 代码。
webFrame.executeJavaScript(code[, userGesture, callback])方法
在当前网页的上下文中执行 JavaScript 代码。userGesture参数表示是否由用户手势触发(如点击事件),callback是执行完成后的回调函数。
示例:
webFrame.executeJavaScript(`
document.body.style.backgroundColor = 'lightblue';
console.log('背景颜色已改变');
`, true, (result) => {
console.log('JavaScript执行结果:', result);
});
webFrame.insertCSS(css[, options])方法
向当前网页插入 CSS 样式。
示例:
webFrame.insertCSS(`
h1 {
color: red;
text-decoration: underline;
}
`);
3.3.3 资源加载控制
webFrame模块还提供了一些方法来控制资源加载行为。
webFrame.registerURLSchemeAsSecure(scheme)方法
注册一个 URL 协议为安全协议,使其可以使用https:相关的 API,如XMLHttpRequest和fetch。
示例:
webFrame.registerURLSchemeAsSecure('myapp');
webFrame.registerURLSchemeAsPrivileged(scheme, options)方法
注册一个 URL 协议为特权协议,使其具有更多权限。
示例:
webFrame.registerURLSchemeAsPrivileged('myapp', {
secure: true,
standard: true,
supportFetchAPI: true
});
3.4 clipboard 模块:剪贴板操作
clipboard模块允许在渲染进程中访问系统剪贴板,实现复制、剪切和粘贴功能。
3.4.1 写入剪贴板
clipboard模块提供了多种方法向剪贴板写入不同类型的数据。
clipboard.writeText(text)方法
向剪贴板写入纯文本数据。
示例:
const { clipboard } = require('electron');
clipboard.writeText('选中的文本');
clipboard.writeHTML(html)方法
向剪贴板写入 HTML 格式的数据。
示例:
clipboard.writeHTML('<strong>加粗文本</strong>');
clipboard.writeImage(image)方法
向剪贴板写入图片数据。image参数是一个NativeImage对象。
示例:
const { NativeImage } = require('electron');
const image = NativeImage.createFromPath('image.png');
clipboard.writeImage(image);
clipboard.write(data)方法
向剪贴板写入多种格式的数据。data参数是一个对象,键是 MIME 类型,值是对应的数据。
示例:
clipboard.write({
'text/plain': '纯文本',
'text/html': '<p>HTML内容</p>'
});
3.4.2 读取剪贴板
clipboard模块还提供了方法从剪贴板读取不同类型的数据。
clipboard.readText()方法
从剪贴板读取纯文本数据。
示例:
const text = clipboard.readText();
console.log('剪贴板中的文本:', text);
clipboard.readHTML()方法
从剪贴板读取 HTML 格式的数据。
示例:
const html = clipboard.readHTML();
console.log('剪贴板中的HTML:', html);
clipboard.readImage()方法
从剪贴板读取图片数据,返回一个NativeImage对象。
示例:
const image = clipboard.readImage();
if (!image.isEmpty()) {
image.saveToPath('clipboard-image.png');
}
clipboard.read(dataTypes)方法
从剪贴板读取指定 MIME 类型的数据。dataTypes参数是一个数组,包含要读取的 MIME 类型。
示例:
const data = clipboard.read(['text/plain', 'text/html']);
console.log('剪贴板中的数据:', data);
3.4.3 剪贴板变化监听
clipboard模块提供了on方法来监听剪贴板内容的变化。
clipboard.on('change', (event, type) => { … })事件
当剪贴板内容发生变化时触发。type参数表示变化的类型,可以是'copy', 'cut', 'paste'或'unknown'。
示例:
clipboard.on('change', (event, type) => {
console.log(`剪贴板内容已改变(类型:${type})`);
console.log('当前剪贴板文本:', clipboard.readText());
});
3.5 screen 模块:屏幕信息和尺寸
screen模块提供了关于屏幕的信息,如显示器的尺寸、工作区域、缩放因子等。
3.5.1 获取屏幕信息
screen模块提供了多种方法获取屏幕相关信息。
screen.getAllDisplays()方法
返回所有显示器的信息数组。每个显示器对象包含以下属性:
- bounds:显示器的边界矩形,包含x, y, width, height。
- workArea:显示器的工作区域矩形(不包含任务栏等系统元素)。
- scaleFactor:显示器的缩放因子。
- id:显示器的唯一标识符。
- rotation:显示器的旋转角度。
示例:
const { screen } = require('electron');
const displays = screen.getAllDisplays();
displays.forEach((display, index) => {
console.log(`显示器 ${index + 1} 信息:`);
console.log(` 边界:${display.bounds.x}, ${display.bounds.y}, ${display.bounds.width}x${display.bounds.height}`);
console.log(` 工作区域:${display.workArea.x}, ${display.workArea.y}, ${display.workArea.width}x${display.workArea.height}`);
console.log(` 缩放因子:${display.scaleFactor}`);
});
screen.getPrimaryDisplay()方法
返回主显示器的信息对象。
示例:
const primaryDisplay = screen.getPrimaryDisplay();
console.log('主显示器工作区域:', primaryDisplay.workArea);
3.5.2 窗口位置计算
screen模块提供了一些方法来帮助计算窗口的位置,使其适应不同的显示器配置。
screen.getCursorScreenPoint()方法
返回当前鼠标指针在屏幕坐标系中的位置。
示例:
const cursorPosition = screen.getCursorScreenPoint();
console.log(`鼠标位置:${cursorPosition.x}, ${cursorPosition.y}`);
screen.getDisplayNearestPoint(point)方法
返回离指定点最近的显示器信息对象。
示例:
const point = { x: 100, y: 100 };
const display = screen.getDisplayNearestPoint(point);
console.log(`离点 (100, 100) 最近的显示器工作区域:`, display.workArea);
screen.getWorkingAreaOfCursor()方法
返回当前鼠标指针所在显示器的工作区域。
示例:
const workingArea = screen.getWorkingAreaOfCursor();
console.log(`鼠标所在显示器的工作区域:${workingArea.x}, ${workingArea.y}, ${workingArea.width}x${workingArea.height}`);
四、进程间通信与高级主题
4.1 ipcMain 模块:主进程通信
ipcMain模块用于在主进程中监听和处理来自渲染进程的消息,是实现进程间通信的关键模块之一。
4.1.1 监听消息
主进程可以使用ipcMain.on(channel, listener)方法监听来自渲染进程的消息。
示例:
const { ipcMain } = require('electron');
ipcMain.on('message-from-renderer', (event, message) => {
console.log('收到渲染进程消息:', message);
// 发送响应消息
event.sender.send('reply-to-renderer', '你好,渲染进程!');
});
event参数说明
listener回调函数的第一个参数是event对象,它包含以下属性和方法:
- event.sender:发送消息的渲染进程对象(WebContents实例)。
- event.preventDefault():阻止默认行为(如果有的话)。
- event.returnValue:设置同步消息的返回值。
- event.send(channel[, …args]):向发送者发送消息。
4.1.2 发送消息到渲染进程
主进程可以通过event.sender.send(channel[, …args])方法向发送消息的渲染进程发送响应。
示例:
ipcMain.on('request-data', (event) => {
const data = fetchData(); // 获取数据的函数
event.sender.send('data-reply', data);
});
如果需要向所有渲染进程发送消息,可以使用BrowserWindow.getAllWindows()方法获取所有窗口,然后遍历发送。
示例:
ipcMain.on('broadcast-message', (event, message) => {
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('global-message', message);
});
});
4.1.3 处理同步消息
主进程可以使用ipcMain.handle(channel, listener)方法处理同步消息请求。
示例:
ipcMain.handle('sync-message', (event, arg) => {
console.log('收到同步消息:', arg);
return '同步响应';
});
在渲染进程中,可以使用ipcRenderer.invoke(channel[, …args])方法发送同步消息并获取响应。
示例:
const response = await ipcRenderer.invoke('sync-message', '同步请求');
console.log('同步响应:', response);
4.2 跨进程数据传递
在 Electron 应用中,经常需要在主进程和渲染进程之间传递数据。本节将介绍几种常见的数据传递方式及其注意事项。
4.2.1 基本数据类型传递
基本数据类型(如字符串、数字、布尔值)可以直接在进程间传递,Electron 会自动进行序列化和反序列化。
示例:
// 主进程
ipcMain.on('send-number', (event, number) => {
console.log('收到数字:', number);
event.sender.send('reply-number', number * 2);
});
// 渲染进程
ipcRenderer.send('send-number', 42);
ipcRenderer.on('reply-number', (event, result) => {
console.log('计算结果:', result); // 输出 84
});
4.2.2 对象和数组传递
复杂数据类型(如对象和数组)也可以在进程间传递,但需要注意以下几点:
示例:
// 主进程
ipcMain.on('send-object', (event, obj) => {
console.log('收到对象:', obj);
obj.answer = 42; // 修改对象不会影响发送方
event.sender.send('reply-object', obj);
});
// 渲染进程
const data = { question: '生命、宇宙以及一切的终极答案' };
ipcRenderer.send('send-object', data);
ipcRenderer.on('reply-object', (event, result) => {
console.log('修改后的对象:', result); // 输出 { question: '…', answer: 42 }
console.log('原始对象:', data); // 输出 { question: '…' }
});
4.2.3 缓冲区传递
对于二进制数据,如文件内容或图像数据,可以使用Buffer对象进行传递。Buffer对象在进程间传递时不会被拷贝,而是通过引用传递,这提高了性能并减少了内存使用。
示例:
// 主进程
const fs = require('fs');
ipcMain.on('read-file', (event, filePath) => {
try {
const buffer = fs.readFileSync(filePath);
event.sender.send('file-content', buffer);
} catch (error) {
event.sender.send('file-error', error.message);
}
});
// 渲染进程
ipcRenderer.send('read-file', 'data.txt');
ipcRenderer.on('file-content', (event, buffer) => {
const content = buffer.toString();
console.log('文件内容:', content);
});
4.2.4 大型数据传递注意事项
传递大型数据时需要注意以下几点:
4.3 远程模块的替代方案
remote模块允许渲染进程直接访问主进程中的对象,但它存在一些安全和性能问题。本节将介绍如何使用ipcRenderer和ipcMain实现相同的功能。
4.3.1 简单方法调用
假设在主进程中有一个createWindow()函数,在渲染进程中需要调用它。使用remote模块可以直接调用:
使用 remote 模块:
const { remote } = require('electron');
remote.createWindow();
替代方案:
在主进程中:
ipcMain.on('create-window', () => {
createWindow();
});
在渲染进程中:
const { ipcRenderer } = require('electron');
ipcRenderer.send('create-window');
4.3.2 带参数的方法调用
如果方法需要参数,可以通过消息传递参数:
使用 remote 模块:
const { remote } = require('electron');
remote.createWindow({ width: 1024, height: 768 });
替代方案:
在主进程中:
ipcMain.on('create-window', (event, options) => {
createWindow(options);
});
在渲染进程中:
const options = { width: 1024, height: 768 };
ipcRenderer.send('create-window', options);
4.3.3 获取返回值
对于有返回值的方法,需要使用异步消息传递:
使用 remote 模块:
const { remote } = require('electron');
const count = remote.getWindowCount();
console.log(`窗口数量:${count}`);
替代方案:
在主进程中:
ipcMain.handle('get-window-count', () => {
return BrowserWindow.getAllWindows().length;
});
在渲染进程中:
const { ipcRenderer } = require('electron');
ipcRenderer.invoke('get-window-count').then(count => {
console.log(`窗口数量:${count}`);
});
4.4 应用打包与分发
开发完成后,需要将 Electron 应用打包成可执行文件,以便用户安装和使用。Electron 提供了多种工具和方法来实现这一点。
4.4.1 使用 Electron Forge 打包
Electron Forge 是 Electron 官方推荐的打包工具,它提供了一站式的解决方案,包括打包、签名、发布等功能。
安装 Electron Forge:
npm install –save-dev electron-forge
初始化 Electron Forge 项目:
npx electron-forge import
这会自动检测项目类型并进行配置。
配置打包选项:
在package.json中,可以配置 Electron Forge 的打包选项:
"config": {
"forge": {
"packagerConfig": {
"asar": true,
"icon": "./build/icon"
},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "my-electron-app"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
}
打包应用:
npm run make
这会在out目录中生成适用于不同平台的安装包。
4.4.2 使用 Electron Packager
Electron Packager 是另一个常用的打包工具,它可以将 Electron 应用打包成适用于不同平台的可执行文件。
安装 Electron Packager:
npm install –save-dev electron-packager
配置打包脚本:
在package.json的scripts部分添加打包命令:
"scripts": {
"package:win32": "electron-packager . my-app –platform=win32 –arch=x64 –icon=build/icon.ico –prune=true –out=release-builds",
"package:darwin": "electron-packager . my-app –platform=darwin –arch=x64 –icon=build/icon.icns –prune=true –out=release-builds",
"package:linux": "electron-packager . my-app –platform=linux –arch=x64 –icon=build/icon.png –prune=true –out=release-builds"
}
执行打包:
npm run package:win32
npm run package:darwin
npm run package:linux
这会在release-builds目录中生成适用于不同平台的可执行文件。
4.4.3 自动更新
为了让用户能够方便地更新应用,可以集成自动更新功能。Electron 提供了electron-updater模块来实现这一点。
安装 electron-updater:
npm install –save-dev electron-updater
主进程中添加自动更新代码:
const { autoUpdater } = require('electron-updater');
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
type: 'info',
title: '更新可用',
message: '应用更新已下载,是否重启应用以安装更新?'
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
autoUpdater.on('error', (error) => {
dialog.showErrorBox('更新错误', error.message);
});
配置更新服务器:
需要将应用的更新文件上传到一个 HTTP 服务器,并在package.json中配置更新地址:
"build": {
"publish": [
{
"provider": "generic",
"url": "https://example.com/update"
}
]
}
五、实战案例:构建简单的桌面工具
5.1 文本编辑器应用
在本节中,我们将使用 Electron 构建一个简单的文本编辑器应用,演示如何综合运用前面学习的 API。
5.1.1 应用功能规划
我们的文本编辑器将具备以下基本功能:
5.1.2 主进程代码
首先,创建主进程文件main.js:
const { app, BrowserWindow, Menu, dialog } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadFile('index.html');
mainWindow.on('closed', () => {
mainWindow = null;
});
// 创建菜单
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => { newFile(); }
},
{
label: '打开…',
accelerator: 'CmdOrCtrl+O',
click: () => { openFile(); }
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => { saveFile(); }
},
{
label: '另存为…',
accelerator: 'Shift+CmdOrCtrl+S',
click: () => { saveFileAs(); }
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => { app.quit(); }
}
]
},
{
label: '编辑',
submenu: [
{
label: '剪切',
accelerator: 'CmdOrCtrl+X',
click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("cut")'); }
},
{
label: '复制',
accelerator: 'CmdOrCtrl+C',
click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("copy")'); }
},
{
label: '粘贴',
accelerator: 'CmdOrCtrl+V',
click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("paste")'); }
},
{ type: 'separator' },
{
label: '全选',
accelerator: 'CmdOrCtrl+A',
click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("selectAll")'); }
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// 文件操作相关函数
let currentFilePath = null;
function newFile() {
currentFilePath = null;
mainWindow.webContents.send('new-file');
}
function openFile() {
dialog.showOpenDialog({
filters: [{ name: '文本文件', extensions: ['txt'] }]
}).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0];
const content = require('fs').readFileSync(filePath, 'utf8');
mainWindow.webContents.send('open-file', filePath, content);
currentFilePath = filePath;
}
});
}
function saveFile() {
if (currentFilePath) {
const content = mainWindow.webContents.executeJavaScript('document.getElementById("editor").value');
require('fs').writeFileSync(currentFilePath, content);
} else {
saveFileAs();
}
}
function saveFileAs() {
dialog.showSaveDialog({
filters: [{ name: '文本文件', extensions: ['txt'] }]
}).then(result => {
if (!result.canceled) {
const filePath = result.filePath;
const content = mainWindow.webContents.executeJavaScript('document.getElementById("editor").value');
require('fs').writeFileSync(filePath, content);
currentFilePath = filePath;
}
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
5.1.3 渲染进程代码
创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron文本编辑器</title>
<style>
body { margin: 0; padding: 0; }
#editor { width: 100vw; height: 100vh; border: none; padding: 10px; box-sizing: border-box; font-size: 16px; }
</style>
</head>
<body>
<textarea id="editor"></textarea>
<script src="renderer.js"></script>
</body>
</html>
renderer.js:
const { ipcRenderer } = require('electron');
// 监听新建文件消息
ipcRenderer.on('new-file', () => {
document.getElementById('editor').value = '';
});
// 监听打开文件消息
ipcRenderer.on('open-file', (event, filePath, content) => {
document.title = `Electron文本编辑器 – ${path.basename(filePath)}`;
document.getElementById('editor').value = content;
});
// 处理窗口关闭时的确认
window.addEventListener('beforeunload', (event) => {
const content = document.getElementById('editor').value;
if (content.trim() !== '') {
event.returnValue = '你有未保存的更改,确定要退出吗?';
}
});
5.1.4 运行和测试
在项目目录中执行以下命令:
npm install electron –save-dev
npm start
测试应用的各项功能:
5.2 文件管理器应用
接下来,我们将构建一个简单的文件管理器应用,展示如何使用 Electron 的文件系统和对话框 API。
5.2.1 应用功能规划
我们的文件管理器将具备以下基本功能:
5.2.2 主进程代码
创建主进程文件main.js:
const { app, BrowserWindow, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadFile('index.html');
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
5.2.3 渲染进程代码
创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron文件管理器</title>
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
.header { background-color: #f0f0f0; padding: 10px; }
.breadcrumb { margin: 10px; }
.file-list { list-type: none; padding: 0; margin: 10px; }
.file-item { padding: 5px; border: 1px solid #ddd; margin-bottom: 5px; cursor: pointer; }
.file-item:hover { background-color: #f5f5f5; }
.file-info { margin: 10px; padding: 10px; border: 1px solid #ddd; }
</style>
</head>
<body>
<div class="header">
<button id="back-btn">返回</button>
<button id="new-file-btn">新建文件</button>
<button id="delete-btn">删除</button>
</div>
<div class="breadcrumb" id="breadcrumb"></div>
<ul class="file-list" id="file-list"></ul>
<div class="file-info" id="file-info"></div>
<script src="renderer.js"></script>
</body>
</html>
renderer.js:
const { ipcRenderer, remote } = require('electron');
const fs = require('fs');
const path = require('path');
let currentPath = remote.app.getPath('documents'); // 初始路径为用户文档目录
// 初始化文件列表
updateFileList(currentPath);
// 监听返回按钮点击
document.getElementById('back-btn').addEventListener('click', () => {
const parentPath = path.dirname(currentPath);
if (parentPath !== currentPath) {
currentPath = parentPath;
updateFileList(currentPath);
}
});
// 监听新建文件按钮点击
document.getElementById('new-file-btn').addEventListener('click', () => {
const fileName = prompt('请输入新文件名:');
if (fileName) {
const filePath = path.join(currentPath, fileName);
fs.writeFileSync(filePath, ''); // 创建空文件
updateFileList(currentPath);
}
});
// 监听文件列表点击事件
document.getElementById('file-list').addEventListener('click', (event) => {
if (event.target.classList.contains('file-item')) {
const fileName = event.target.dataset.filename;
const filePath = path.join(currentPath, fileName);
if (fs.statSync(filePath).isDirectory()) {
currentPath = filePath;
updateFileList(currentPath);
} else {
showFileInfo(filePath);
}
}
});
// 更新文件列表
function updateFileList(path) {
const fileList = document.getElementById('file-list');
fileList.innerHTML = '';
// 更新面包屑导航
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = generateBreadcrumb(path);
try {
const files = fs.readdirSync(path, { withFileTypes: true });
files.forEach(file => {
const fileItem = document.createElement('li');
fileItem.className = 'file-item';
fileItem.dataset.filename = file.name;
fileItem.innerHTML = `
<span>${file.name}</span>
<span>${file.isDirectory() ? '目录' : '文件'}</span>
`;
fileList.appendChild(fileItem);
});
} catch (error) {
alert('无法读取目录:' + error.message);
}
}
// 生成面包屑导航
function generateBreadcrumb(path) {
const parts = path.split(path.sep);
return parts.map((part, index) => {
const href = parts.slice(0, index + 1).join(path.sep);
return `<a href="#" onclick="navigateTo('${href}')">${part}</a>`;
}).join(' / ');
}
// 导航到指定路径
window.navigateTo = function(href) {
currentPath = href;
updateFileList(currentPath);
};
// 显示文件信息
function showFileInfo(filePath) {
const fileInfo = document.getElementById('file-info');
try {
const stats = fs.statSync(filePath);
fileInfo.innerHTML = `
<p><strong>文件名:</strong> ${path.basename(filePath)}</p>
<p><strong>路径:</strong> ${filePath}</p>
<p><strong>类型:</strong> ${stats.isDirectory() ? '目录' : '文件'}</p>
<p><strong>大小:</strong> ${formatSize(stats.size)}</p>
<p><strong>创建时间:</strong> ${stats.birthtime.toLocaleString()}</p>
<p><strong>修改时间:</strong> ${stats.mtime.toLocaleString()}</p>
`;
} catch (error) {
fileInfo.innerHTML = '无法获取文件信息:' + error.message;
}
}
// 格式化文件大小
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
else if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
else if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
else return `${(bytes / 1073741824).toFixed(1)} GB`;
}
5.2.4 运行和测试
在项目目录中执行以下命令:
npm install electron –save-dev
npm start
测试应用的各项功能:
5.3 图像查看器应用
最后,我们将构建一个简单的图像查看器应用,展示如何使用 Electron 的图像处理和文件操作 API。
5.3.1 应用功能规划
我们的图像查看器将具备以下基本功能:
5.3.2 主进程代码
创建主进程文件main.js:
const { app, BrowserWindow, dialog } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadFile('index.html');
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
5.3.3 渲染进程代码
创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron图像查看器</title>
<style>
body { margin: 0; padding: 0; }
.toolbar { background-color: #f0f0f0; padding: 10px; }
#image-container { display: flex; justify-content: center; align-items: center; height: calc(100vh – 40px); }
#image { max-width: 100%; max-height: 100%; }
</style>
</head>
<body>
<div class="toolbar">
<button id="open-btn">打开图像</button>
<button id="save-btn">保存</button>
<button id="zoom-in-btn">放大</button>
<button id="zoom-out-btn">缩小</button>
<button id="rotate-btn">旋转</button>
</div>
<div id="image-container">
<img id="image" src="">
</div>
<script src="renderer.js"></script>
</body>
</html>
renderer.js:
const { ipcRenderer, remote } = require('electron');
const fs = require('fs');
const path = require('path');
const { nativeImage } = require('electron');
let currentImagePath = null;
let image = null;
let zoomFactor = 1;
let rotation = 0;
// 初始化
document.getElementById('image').style.transform = `scale(${zoomFactor}) rotate(${rotation}deg)`;
// 监听打开图像按钮点击
document.getElementById('open-btn').addEventListener('click', () => {
ipcRenderer.invoke('open-image').then(filePath => {
if (filePath) {
currentImagePath = filePath;
image = nativeImage.createFromPath(currentImagePath);
document.getElementById('image').src = image.toDataURL();
}
});
});
// 监听保存按钮点击
document.getElementById('save-btn').addEventListener('click', () => {
if (image) {
ipcRenderer.invoke('save-image', currentImagePath, image).then(success => {
if (success) {
alert('图像已保存');
} else {
alert('保存失败');
}
});
}
});
// 监听放大按钮点击
document.getElementById('zoom-in-btn').addEventListener('click', () => {
zoomFactor *= 1.2;
updateImageDisplay();
});
// 监听缩小按钮点击
document.getElementById('zoom-out-btn').addEventListener('click', () => {
zoomFactor /= 1.2;
updateImageDisplay();
});
// 监听旋转按钮点击
document.getElementById('rotate-btn').addEventListener('click', () => {
rotation += 90;
updateImageDisplay();
});
// 更新图像显示
function updateImageDisplay() {
const imageElement = document.getElementById('image');
imageElement.style.transform = `scale(${zoomFactor}) rotate(${rotation}deg)`;
}
// 处理窗口关闭时的确认
window.addEventListener('beforeunload', (event) => {
if (image) {
event.returnValue = '你有未保存的更改,确定要退出吗?';
}
});
// 主进程通信
ipcRenderer.on('open-image', (event, filePath) => {
if (filePath) {
currentImagePath = filePath;
image = nativeImage.createFromPath(currentImagePath);
document.getElementById('image').src = image.toDataURL();
}
});
ipcRenderer.handle('open-image', () => {
return dialog.showOpenDialog({
filters: [{ name: '图像文件', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp'] }]
}).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
} else {
return null;
}
});
});
ipcRenderer.handle('save-image', (event, filePath, image) => {
return new Promise((resolve) => {
if (!filePath) {
dialog.showSaveDialog({
filters: [{ name: '图像文件', extensions: ['png'] }]
}).then(result => {
if (!result.canceled) {
filePath = result.filePath;
image.writePNG(filePath, () => {
resolve(true);
});
} else {
resolve(false);
}
});
} else {
image.writePNG(filePath, () => {
resolve(true);
});
}
});
});
5.3.4 运行和测试
在项目目录中执行以下命令:
npm install electron –save-dev
npm start
测试应用的各项功能:
六、总结与进阶学习
6.1 本书内容回顾
在本书中,我们从零开始学习了 Electron 框架的基础知识和核心 API,包括:
6.2 最佳实践与注意事项
在使用 Electron 开发应用时,需要遵循以下最佳实践和注意事项:
-
- 避免在渲染进程中使用remote模块,优先使用ipcRenderer和ipcMain进行通信。
-
- 谨慎启用 Node.js 集成(nodeIntegration),只在必要时使用。
-
- 对来自用户的输入进行安全处理,防止代码注入攻击。
-
- 避免在主进程中执行阻塞操作,使用异步 API 或工作线程。
-
- 对于大型数据传递,考虑使用Buffer对象或流式传输。
-
- 合理管理窗口资源,及时销毁不再使用的窗口。
-
- 测试应用在不同操作系统上的行为和外观。
-
- 使用process.platform检测当前平台,为不同平台提供适当的处理。
-
- 遵循各平台的用户界面规范,如 macOS 的菜单栏和 Windows 的任务栏。
-
- 将主进程和渲染进程的代码分离,保持代码清晰。
-
- 使用事件驱动架构,避免紧密耦合。
-
- 为 Electron API 调用添加错误处理,提高应用的稳定性。
6.3 进阶学习资源
如果你希望进一步深入学习 Electron,以下是一些推荐的资源:
6.4 结语
Electron 为 Web 开发者提供了一个强大的工具,使他们能够利用现有的技能创建跨平台桌面应用。通过本书的学习,你应该已经掌握了 Electron 的基础知识和核心 API,能够开发简单的桌面工具应用。
记住,学习任何框架的最佳方法是实践。尝试将本书中学到的知识应用到实际项目中,解决实际问题。随着你对 Electron 的深入理解,你会发现它的潜力远远不止于此,能够创建出功能更强大、体验更丰富的桌面应用。
希望本书能够成为你学习 Electron 的起点,帮助你开启桌面应用开发的新旅程!
评论前必须登录!
注册