欢迎来到 Electron 桌面应用开发的世界!本教程将带您从零开始,使用您熟悉的 Web 技术(HTML、CSS、JavaScript)构建功能强大的跨平台桌面应用程序。
想象一下,您用 HTML 写了一个漂亮的网页,用 CSS 美化了它,再用 JavaScript 赋予了它各种交互功能。现在,您想把这个"网页"变成一个独立运行在 Windows、macOS 或 Linux 系统上的桌面程序,就像 VS Code、Slack 或者 Figma 那样。Electron 就是实现这个梦想的神奇工具!
Electron 是由 GitHub 开发的一个开源框架,它允许开发者使用Web 技术(HTML、CSS 和 JavaScript)构建原生的跨平台桌面应用程序。它通过将 Chromium(Google Chrome 浏览器核心)和 Node.js(JavaScript 运行时)集成到一个软件包中来实现这一点。
这是理解 Electron 最最核心的两个概念。可以把它们想象成一场复杂的演出中的两位主角:
Electron 的强大之处在于它将 Node.js 的能力带到了桌面应用程序中。这意味着您可以在主进程中使用所有 Node.js 模块(例如 fs 模块进行文件操作,http 模块进行网络请求等),这极大地扩展了 Web 技术的边界,让您的应用能够做更多原生应用才能做的事情。
开始之前,我们需要准备好开发环境。
Electron 基于 Node.js 运行,并使用 npm (Node Package Manager) 或 yarn 进行包管理。如果您尚未安装,请前往 Node.js 官网 下载并安装 LTS (长期支持) 版本。安装完成后,您可以通过命令行检查版本:
node -v
npm -v
# 或者如果您使用 yarn
yarn -v
您应该会看到类似 `v18.x.x` 和 `9.x.x` 的版本号。
创建一个新的文件夹作为您的项目目录,然后进入该目录,使用 npm 或 yarn 初始化一个新的 Node.js 项目:
mkdir my-electron-app
cd my-electron-app
npm init -y
# 或者
yarn init -y
这会在您的项目根目录创建一个 `package.json` 文件,它是 Node.js 项目的配置文件。
接下来,安装 Electron 作为您项目的开发依赖:
npm install --save-dev electron
# 或者
yarn add --dev electron
安装完成后,`package.json` 文件中会多出 `electron` 这一项。
一个最基本的 Electron 应用通常需要三个文件:
`main.js`:
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow () {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// 强烈建议禁用 Node.js 集成,并通过预加载脚本安全地暴露API
nodeIntegration: false,
// 开启上下文隔离,这是更安全的做法,防止恶意脚本访问Electron内部API
contextIsolation: true,
preload: path.join(__dirname, 'preload.js') // 预加载脚本
}
});
// 加载 index.html 文件
mainWindow.loadFile('index.html');
// 打开开发者工具 (可选,方便调试)
// mainWindow.webContents.openDevTools();
}
// 当 Electron 应用准备就绪时,调用 createWindow
app.whenReady().then(() => {
createWindow();
// macOS 特定:当所有窗口都关闭时,如果用户没有显式退出,应用会保持活跃状态
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 当所有窗口都关闭时退出应用
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户显式用 Cmd + Q 退出,否则应用通常保持活跃
if (process.platform !== 'darwin') {
app.quit();
}
});
`index.html`:
我的第一个 Electron 应用
Hello, Electron World! 👋
`preload.js` (安全桥梁): 这是一个在主进程和渲染进程之间,但在渲染进程的 Web 内容加载之前运行的脚本。它是连接 Node.js API 和渲染进程的安全桥梁。
// preload.js (暂时留空,后续会详细讲解)
// const { contextBridge, ipcRenderer } = require('electron');
// contextBridge.exposeInMainWorld('electronAPI', {
// // 这里可以暴露主进程提供的安全API
// });
`renderer.js` (渲染进程脚本): 暂时留空,或者放入一些简单的JavaScript来验证HTML加载成功。
// renderer.js
console.log('Electron 渲染进程已加载!');
修改 `package.json`:
在 `scripts` 部分添加一个 `start` 命令,指向您的主进程文件:
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "My first Electron application",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^28.0.0"
}
}
确保将 `"main": "main.js"` 设置为主进程的入口文件。
现在,您可以在命令行中运行您的应用了:
npm start
# 或者
yarn start
如果一切顺利,您应该会看到一个标题为"我的第一个 Electron 应用"的窗口弹出,里面显示着"Hello, Electron World! 👋"。恭喜您,您的第一个 Electron 应用成功运行了!
上图清晰地展示了 Electron 应用中主进程与渲染进程的关系以及它们如何协同工作。预加载脚本作为中间层,确保了安全通信。
主进程是 Electron 应用的"大脑",它拥有访问操作系统底层 API 的能力。以下是主进程中常用的模块和功能:
BrowserWindow 是 Electron 中用于创建新窗口的类。您可以通过它来定义窗口的大小、位置、是否可调整大小、是否带边框等属性。
// main.js (片段)
const { BrowserWindow } = require('electron');
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
minWidth: 400, // 最小宽度
minHeight: 300, // 最小高度
frame: true, // 是否显示系统边框 (true为显示,false为无边框窗口)
transparent: false, // 是否透明 (需要 frame: false 配合)
webPreferences: {
nodeIntegration: false, // 禁用 Node.js API
contextIsolation: true, // 开启上下文隔离
preload: path.join(__dirname, 'preload.js')
}
});
// 加载本地 HTML 文件
mainWindow.loadFile('index.html');
// 也可以加载远程 URL,用于 Web 应用的桌面化
// mainWindow.loadURL('https://www.electronjs.org');
// 监听窗口关闭事件
mainWindow.on('closed', () => {
// 销毁窗口引用,防止内存泄漏。
// 在 Electron 20+ 中,当窗口关闭时,其引用通常会自动垃圾回收,
// 但如果您的应用有多个窗口且需要精确管理,仍可手动置空。
});
}
app 模块控制着 Electron 应用程序的事件生命周期。您已经看到过它的使用,比如 `app.whenReady()` 和 `app.on('window-all-closed')`。
Electron 允许您创建自定义的应用程序菜单和系统托盘图标。
// main.js (片段)
const { app, BrowserWindow, Menu, Tray, dialog } = require('electron');
const path = require('path');
let tray = null; // 声明一个全局变量来保持 Tray 实例的引用
// 创建系统托盘图标
function createTray() {
// __dirname 指向当前文件目录,确保 'icon.png' 存在于此
tray = new Tray(path.join(__dirname, 'assets', 'icon.png')); // 建议将图标放在 assets 文件夹
const contextMenu = Menu.buildFromTemplate([
{ label: '打开应用', click: () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); // 如果没有窗口,则创建一个新窗口
} else {
// 如果有窗口,可以尝试聚焦或显示主窗口
BrowserWindow.getAllWindows()[0].show();
}
}},
{ type: 'separator' }, // 分隔线
{ label: '退出', click: () => app.quit() }
]);
tray.setToolTip('我的 Electron 应用'); // 鼠标悬停时的提示
tray.setContextMenu(contextMenu); // 设置右键菜单
}
app.whenReady().then(() => {
createWindow();
// createTray(); // 在这里调用以创建系统托盘
// 创建应用菜单 (顶部菜单栏)
const template = [
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: () => { /* 新建逻辑 */ } },
{ type: 'separator' }, // 分隔线
{ label: '退出', role: 'quit' } // 'quit' 是一个预定义的角色,可以直接退出应用
]
},
{
label: '编辑',
role: 'editMenu' // 使用预定义角色自动填充标准编辑菜单
},
{
label: '视图',
role: 'viewMenu' // 使用预定义角色自动填充标准视图菜单
},
{
label: '帮助',
submenu: [
{ label: '关于', click: () => dialog.showMessageBox({
title: '关于我的应用',
message: `版本:${app.getVersion()}\n作者:My Awesome App`
})}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu); // 设置应用程序菜单
});
注意: `Tray` 图标文件通常是 `.png` 或 `.ico` 格式,需要放在项目目录下,并确保路径正确,例如创建一个 `assets` 文件夹存放。
dialog 模块提供 API 用于显示原生操作系统的文件打开/保存对话框、消息框等。
// main.js (片段)
const { dialog } = require('electron');
const fs = require('fs'); // Node.js 文件系统模块
// 示例:显示一个消息框
async function showCustomMessageBox() {
const result = await dialog.showMessageBox({
type: 'info',
title: '我的应用',
message: '这是一个来自主进程的消息!',
detail: '您可以在这里提供更多详情信息。',
buttons: ['确定', '取消']
});
console.log(`用户点击了按钮索引: ${result.response}`); // 0 for '确定', 1 for '取消'
return result.response;
}
// 示例:打开文件对话框并读取文件内容
async function openAndReadFile() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'], // 允许选择文件
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] },
{ name: '所有文件', extensions: ['*'] }
]
});
if (!canceled && filePaths.length > 0) {
const filePath = filePaths[0];
try {
const content = fs.readFileSync(filePath, 'utf-8');
console.log('文件内容:', content);
return { success: true, content: content };
} catch (err) {
console.error('读取文件失败:', err);
dialog.showErrorBox('文件读取错误', `无法读取文件: ${err.message}`);
return { success: false, error: err.message };
}
}
return { success: false, cancelled: true };
}
// 这些函数通常会通过 IPC 暴露给渲染进程调用
// 例如:ipcMain.handle('show-msg-box', showCustomMessageBox);
// 例如:ipcMain.handle('open-and-read-file', openAndReadFile);
由于主进程是 Node.js 环境,您可以直接使用 Node.js 的核心模块,比如 fs (File System) 来进行文件和目录的操作。
// main.js (片段)
const fs = require('fs');
const path = require('path');
const { app } = require('electron'); // 获取 app 模块来获取路径
// 获取应用程序的用户数据目录
const userDataPath = app.getPath('userData');
const demoFilePath = path.join(userDataPath, 'demo.txt');
// 写入文件 (异步)
function writeDemoFile(content) {
fs.writeFile(demoFilePath, content, 'utf-8', (err) => {
if (err) {
console.error('写入文件失败:', err);
} else {
console.log(`${demoFilePath} 写入成功!`);
}
});
}
// 读取文件 (同步)
function readDemoFile() {
try {
if (fs.existsSync(demoFilePath)) {
const content = fs.readFileSync(demoFilePath, 'utf-8');
console.log(`${demoFilePath} 内容:`, content);
return content;
} else {
console.log(`${demoFilePath} 不存在。`);
return null;
}
} catch (err) {
console.error('读取文件失败:', err);
return null;
}
}
// 使用示例:
// app.whenReady().then(() => {
// writeDemoFile('Hello from Electron!');
// setTimeout(() => { // 稍后读取,确保写入完成
// readDemoFile();
// }, 1000);
// });
渲染进程是您的应用程序界面所在的地方,它本质上就是一个 Chrome 浏览器页面。您可以使用您熟悉的所有 Web 技术来构建界面。
一切如常。您在浏览器中开发的经验可以直接复制到 Electron 渲染进程中。例如,创建一个计数器应用:
`index.html`:
计数器应用
简单的计数器
0
`renderer.js`: (放在与 `index.html` 同级目录)
// renderer.js
let count = 0;
const counterElement = document.getElementById('counter');
const increaseButton = document.getElementById('increase');
const decreaseButton = document.getElementById('decrease');
increaseButton.addEventListener('click', () => {
count++;
counterElement.textContent = count;
});
decreaseButton.addEventListener('click', () => {
count--;
counterElement.textContent = count;
});
这是 Electron 开发中非常重要的一环。由于渲染进程不能直接访问 Node.js API,它需要通过 IPC (Inter-Process Communication) 机制与主进程进行通信。Electron 提供了 `ipcMain` (主进程) 和 `ipcRenderer` (渲染进程) 模块来实现这一点。
安全警告: 在 Electron 12 之后,默认启用了 `contextIsolation: true` 和 `nodeIntegration: false`,这是为了安全。这意味着渲染进程无法直接访问 `require` 或 Electron API。为了安全地与主进程通信,我们强烈推荐使用预加载脚本 (Preload Script)。
主进程可以通过特定窗口的 `webContents.send()` 方法向其渲染进程发送消息。
`main.js` (片段):
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
let mainWindow;
function createWindow () {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 引入预加载脚本
nodeIntegration: false, // 禁用 Node.js 集成,更安全
contextIsolation: true // 开启上下文隔离,更安全
}
});
mainWindow.loadFile('index.html');
// mainWindow.webContents.openDevTools();
// 假设在某个事件后,主进程想给渲染进程发消息
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('main-process-message', '你好,我是主进程!');
}
}, 3000); // 3秒后发送消息
}
app.whenReady().then(createWindow);
// ... 其他生命周期事件 ...
渲染进程通过 `ipcRenderer` 模块发送消息。注意: 由于 `contextIsolation: true`,我们不能直接在渲染进程中使用 `require('electron')`。我们需要通过预加载脚本来"暴露"所需的 API。
`preload.js`:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 渲染进程向主进程发送消息(单向)
sendMessageToMain: (message) => ipcRenderer.send('renderer-message', message),
// 渲染进程向主进程发送消息并等待回复(双向)
invokeMainProcess: (channel, args) => ipcRenderer.invoke(channel, args),
// 监听主进程发来的消息
onMainProcessMessage: (callback) => ipcRenderer.on('main-process-message', (_event, value) => callback(value))
});
`main.js` (接收渲染进程消息):
// main.js (片段)
const { app, BrowserWindow, ipcMain } = require('electron');
// ... createWindow 函数 ...
app.whenReady().then(createWindow);
// 监听渲染进程发送的单向消息
ipcMain.on('renderer-message', (event, message) => {
console.log('收到渲染进程消息:', message); // "你好,我是渲染进程!"
// 这是一个单向通信的例子,通常不直接回复
});
// 监听渲染进程发送的双向消息 (invoke/handle)
ipcMain.handle('get-app-version', async (event) => {
const version = app.getVersion();
console.log('渲染进程请求应用版本:', version);
return version; // 返回数据给渲染进程
});
// ... 其他生命周期事件 ...
`renderer.js` (发送和接收消息):
// renderer.js
// 通过 window.electronAPI 访问预加载脚本暴露的 API
document.addEventListener('DOMContentLoaded', () => {
const messageButton = document.createElement('button');
messageButton.textContent = '向主进程发送消息';
document.body.appendChild(messageButton);
messageButton.addEventListener('click', () => {
// 单向发送
window.electronAPI.sendMessageToMain('你好,我是渲染进程!');
});
const versionButton = document.createElement('button');
versionButton.textContent = '获取应用版本';
document.body.appendChild(versionButton);
versionButton.addEventListener('click', async () => {
// 双向通信:请求并等待回复
const version = await window.electronAPI.invokeMainProcess('get-app-version');
alert(`应用版本:${version}`);
});
// 监听主进程发来的消息
window.electronAPI.onMainProcessMessage((message) => {
const p = document.createElement('p');
p.textContent = `主进程说:${message}`;
document.body.appendChild(p);
});
});
开发完成后,您需要将应用打包成可执行文件,以便在不同操作系统上分发给用户。最流行的工具是 `electron-builder`。
首先,安装 `electron-builder` 作为开发依赖:
npm install --save-dev electron-builder
# 或者
yarn add --dev electron-builder
然后在 `package.json` 中添加配置和打包脚本:
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "My first Electron application",
"main": "main.js",
"scripts": {
"start": "electron .",
"dist": "electron-builder",
"postinstall": "electron-builder install-app-deps"
},
"author": "Your Name",
"license": "ISC",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.0.0"
},
"build": {
"appId": "com.yourcompany.yourappname",
"productName": "我的Electron应用",
"copyright": "Copyright © 2024 ${author}",
"directories": {
"output": "dist"
},
"files": [
"**/*"
],
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "assets/icon.icns"
},
"linux": {
"target": "AppImage",
"icon": "assets/icon.png"
}
}
}
最后,运行打包命令:
npm run dist
# 或者
yarn dist
这将在项目的 `dist` 目录下生成针对不同操作系统的安装包或可执行文件。
代码签名是为您的应用程序"盖章"的过程,以证明它来自可信的发布者,并且自发布以来没有被篡改。这对于桌面应用程序的安全性至关重要,特别是 macOS 和 Windows。没有签名的应用在这些系统上可能会被标记为"不受信任"或"来自未知开发者",从而阻止用户安装。
代码签名配置通常在 `electron-builder` 的 `build` 配置中完成,涉及到证书路径和密码等敏感信息。对于初学者来说,这一步可以暂时跳过,但在发布正式产品时是必不可少的。
我们之前已经初步了解了 IPC,现在深入一下其安全性和最佳实践。
默认情况下,在 Electron 12+ 中,`contextIsolation` 属性在 `webPreferences` 中是 `true`。这意味着:
上图展示了上下文隔离如何将渲染进程的 Web 上下文与 Node.js 上下文隔离开来,从而防止恶意脚本直接访问 Node.js API。通过预加载脚本安全地暴露有限的 API 是最佳实践。
为什么这很重要? 如果 `nodeIntegration` 开启且 `contextIsolation` 关闭,渲染进程中的任何外部 JavaScript 代码(例如来自 CDN 的脚本)都可能获得完整的 Node.js 访问权限,这可能导致严重的安全漏洞。
预加载脚本是解决 `nodeIntegration: false` 和 `contextIsolation: true` 情况下渲染进程无法直接访问 Node.js API 的关键。
它的作用:
最佳实践:
桌面应用通常需要存储用户数据、配置等。Electron 提供了多种存储选项:
如果您只需要在渲染进程中存储少量非敏感数据,可以直接使用浏览器原生的 `localStorage` 或 `IndexedDB`。
`localStorage` (简单键值对存储):
// 渲染进程 (renderer.js)
// 存储数据
localStorage.setItem('username', 'Alice');
// 读取数据
const username = localStorage.getItem('username');
console.log('用户名:', username);
`IndexedDB` (更复杂、异步的结构化数据存储): 适用于需要存储大量结构化数据或离线访问数据的场景。使用方式与 Web 开发中无异。
对于需要直接读写文件系统的数据(如配置文件、用户文档等),应在主进程中使用 Node.js 的 `fs` 模块,并通过 IPC 与渲染进程交互。这通常是更灵活和强大的方式。
// main.js (片段) - 写入和读取 JSON 配置
const fs = require('fs');
const path = require('path');
const { app, ipcMain } = require('electron');
const configPath = path.join(app.getPath('userData'), 'config.json');
ipcMain.handle('save-config', async (event, config) => {
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return { success: true };
} catch (error) {
console.error('保存配置失败:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('load-config', async (event) => {
try {
if (fs.existsSync(configPath)) {
const data = fs.readFileSync(configPath, 'utf-8');
return { success: true, config: JSON.parse(data) };
}
return { success: true, config: {} }; // 如果文件不存在,返回空对象
} catch (error) {
console.error('加载配置失败:', error);
return { success: false, error: error.message };
}
});
`app.getPath('userData')`: 这是一个非常重要的 Electron API,它返回应用程序的用户数据目录路径。这是存储用户特定文件(如配置文件、数据库)的最佳位置,因为它在不同操作系统上都保持一致且通常具有写入权限。
`preload.js` (片段) - 暴露 API:
// preload.js (片段)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('configAPI', {
save: (config) => ipcRenderer.invoke('save-config', config),
load: () => ipcRenderer.invoke('load-config')
});
`renderer.js` (片段) - 使用 API:
// renderer.js (片段)
document.addEventListener('DOMContentLoaded', async () => {
// 假设有一个按钮用于加载配置
const loadButton = document.createElement('button');
loadButton.textContent = '加载配置';
document.body.appendChild(loadButton);
loadButton.addEventListener('click', async () => {
const { success, config, error } = await window.configAPI.load();
if (success) {
console.log('加载到的配置:', config);
alert('加载到的配置: ' + JSON.stringify(config));
} else {
console.error('加载配置失败:', error);
alert('加载配置失败: ' + error);
}
});
// 假设有一个按钮用于保存配置
const saveButton = document.createElement('button');
saveButton.textContent = '保存配置';
document.body.appendChild(saveButton);
saveButton.addEventListener('click', async () => {
const newConfig = { theme: 'dark', notifications: true };
const saveResult = await window.configAPI.save(newConfig);
if (saveResult.success) {
console.log('配置保存成功!');
alert('配置保存成功!');
} else {
console.error('配置保存失败:', saveResult.error);
alert('配置保存失败: ' + saveResult.error);
}
});
});
对于更复杂的数据管理,您可以集成本地数据库,如 SQLite。最常见的方式是使用 Node.js 的 SQLite 驱动(如 `sqlite3` 或 `better-sqlite3`),并在主进程中进行数据库操作。
// main.js (片段) - SQLite 示例 (需要安装 'better-sqlite3')
// npm install better-sqlite3
const Database = require('better-sqlite3');
const dbPath = path.join(app.getPath('userData'), 'my_database.db');
let db;
ipcMain.handle('init-db', async () => {
try {
db = new Database(dbPath);
// 创建表
db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
completed INTEGER DEFAULT 0
)
`);
return { success: true };
} catch (error) {
console.error('数据库初始化失败:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('add-todo-db', async (event, todoText) => {
try {
const stmt = db.prepare('INSERT INTO todos (text) VALUES (?)');
const info = stmt.run(todoText);
return { success: true, id: info.lastInsertRowid };
} catch (error) {
console.error('添加待办失败:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-todos-db', async () => {
try {
const todos = db.prepare('SELECT * FROM todos').all();
return { success: true, todos: todos };
} catch (error) {
console.error('获取待办失败:', error);
return { success: false, error: error.message };
}
});
// 在应用退出前,确保关闭数据库连接 (FIXED)
app.on('before-quit', () => {
if (db && db.open) { // 先检查数据库是否已连接
db.close(); // 然后关闭它
console.log('数据库连接已关闭。');
}
});
注意: 使用 `better-sqlite3` 这样的原生模块时,打包时可能需要额外配置 `electron-builder` 以确保二进制文件被正确包含。
为您的桌面应用提供自动更新是提升用户体验的关键。Electron 内置了 `autoUpdater` 模块,通常与 `electron-updater` 库配合使用。
`electron-updater` 是一个简化自动更新流程的库,它基于 Electron 的 `autoUpdater` 模块。
首先,安装 `electron-updater`:
npm install electron-updater
# 或者
yarn add electron-updater
然后,在主进程 (`main.js`) 中配置和使用:
// main.js (片段) - 自动更新
const { app, BrowserWindow, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path'); // 确保引入 path
const log = require('electron-log'); // 推荐使用日志库
// 配置日志,方便调试 (可选,但强烈推荐)
log.transports.file.level = 'info';
autoUpdater.logger = log;
// 您可以禁用自动下载,手动触发
// autoUpdater.autoDownload = false;
function setupAutoUpdater() {
// 检查更新
autoUpdater.checkForUpdatesAndNotify();
// 更新错误时触发
autoUpdater.on('error', (err) => {
log.error('更新出错:', err);
dialog.showErrorBox('更新失败', `检查更新时发生错误: ${err.message}`);
});
// 检测到新版本时触发
autoUpdater.on('update-available', (info) => {
log.info('发现新版本:', info.version);
dialog.showMessageBox({
type: 'info',
title: '发现新版本',
message: `发现新版本 ${info.version},是否现在下载?`,
buttons: ['立即下载', '稍后']
}).then(result => {
if (result.response === 0) { // 用户点击了"立即下载"
autoUpdater.downloadUpdate();
}
});
});
// 没有可用更新时触发
autoUpdater.on('update-not-available', () => {
log.info('当前已是最新版本。');
});
// 更新下载进度时触发
autoUpdater.on('download-progress', (progressObj) => {
let log_message = `下载速度: ${(progressObj.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`;
log_message += ` - 已下载 ${progressObj.percent.toFixed(2)}%`;
log_message += ` (${(progressObj.transferred / (1024 * 1024)).toFixed(2)} MB / ${(progressObj.total / (1024 * 1024)).toFixed(2)} MB)`;
log.info(log_message);
// 可以通过 IPC 发送进度到渲染进程显示
// mainWindow.webContents.send('download-progress', progressObj.percent);
});
// 下载完成时触发
autoUpdater.on('update-downloaded', () => {
log.info('新版本下载完成。');
dialog.showMessageBox({
type: 'info',
title: '更新准备就绪',
message: '新版本已下载完成,应用将立即重启以安装更新。',
buttons: ['立即重启']
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall(); // 退出并安装新版本
}
});
});
}
app.whenReady().then(() => {
createWindow();
setupAutoUpdater();
});
注意: `electron-updater` 通常与 GitHub Releases 或您自己的更新服务器配合使用。您需要将打包好的应用发布到这些平台上,`electron-updater` 才能检测到新版本。
Electron 应用本质上是浏览器和 Node.js 的结合,因此性能优化和调试方法与 Web 和 Node.js 应用有很多共通之处。
每个 Electron 窗口 (渲染进程) 都可以像 Chrome 浏览器标签页一样打开开发者工具。在 `BrowserWindow` 创建后,调用 `mainWindow.webContents.openDevTools();` 即可打开。您可以使用它来:
对于主进程,您可以使用 VS Code 等 IDE 的调试功能(通过 `F5` 启动调试配置),或者通过 Chrome 开发者工具连接到 Node.js 进程进行调试(在命令行启动应用时添加 `--inspect` 或 `--inspect-brk` 参数)。
构建 Electron 应用时,安全性是重中之重,尤其当您的应用需要处理用户数据或访问网络时。
这是最重要的安全设置。
请始终在 `webPreferences` 中使用它们,并配合预加载脚本 (Preload Script) 来安全地暴露所需功能。
// main.js (片段)
new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 禁用 Node.js API 在渲染进程的直接访问
contextIsolation: true, // 开启上下文隔离
preload: path.join(__dirname, 'preload.js') // 必须使用预加载脚本来桥接
}
});
与 Web 应用类似,您可以在 HTML 文件中使用 Content Security Policy (CSP) 来限制渲染进程可以加载的资源(脚本、样式、图片等),从而防止跨站脚本攻击 (XSS)。
上述 CSP 策略意味着:
重要: `unsafe-inline` 和 `unsafe-eval` 应该被视为最后手段。如果可能,应将所有脚本和样式外部化,并使用哈希值或 Nonce 值来允许它们。
当用户点击渲染进程中的一个链接时,如果链接是外部网站,应该在用户默认浏览器中打开,而不是在 Electron 窗口中。这可以防止将敏感的 Node.js/Electron API 暴露给外部网站。
`main.js` (片段):
// main.js (片段)
const { shell } = require('electron');
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl);
// 仅允许加载应用内部的协议 (如 file:)
if (parsedUrl.protocol !== 'file:') {
event.preventDefault();
shell.openExternal(navigationUrl); // 在默认浏览器中打开外部链接
}
});
});
Electron 应用作为前端技术的延伸,可以无缝集成各种流行的前端框架和 UI 库。
将 React、Vue 或 Angular 项目打包成 Electron 应用非常常见。基本流程是:
// main.js (React/Vue/Angular 集成)
// const path = require('path');
// mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); // 假设您的构建输出在项目根目录的 'dist' 文件夹
这些 UI 库通常是基于 React、Vue 或纯 Web Component 的。集成方式与集成前端框架类似,只要它们能在标准浏览器环境中运行,就能在 Electron 渲染进程中运行。
只需在您的前端项目中安装并引入这些 UI 库,然后在您的组件中使用它们即可。
理论知识学习了这么多,让我们通过一个简单的实战项目来巩固!我们将构建一个具备以下功能的待办事项应用:
首先,在您的项目根目录创建以下文件和文件夹:
my-todo-app/
├── main.js # Electron 主进程脚本
├── index.html # 渲染进程界面
├── renderer.js # 渲染进程逻辑脚本
├── preload.js # 预加载脚本 (用于安全地桥接主/渲染进程)
├── package.json # 项目配置文件
└── assets/ # (新增) 存放图标等静态资源
└── icon.png
└── icon.ico
└── icon.icns
确保您的 `package.json` 配置了 `main.js` 和 `start` 脚本,并且安装了 `electron`。
主进程将负责:
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow;
// 将数据文件放在 userData 目录下,确保跨平台持久化和写入权限
const dataFilePath = path.join(app.getPath('userData'), 'todos.json');
function createWindow () {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
mainWindow.loadFile('index.html');
// mainWindow.webContents.openDevTools(); // 调试用
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// IPC 处理:保存待办事项数据
ipcMain.handle('save-todos', async (event, todos) => {
try {
fs.writeFileSync(dataFilePath, JSON.stringify(todos, null, 2), 'utf-8');
return { success: true };
} catch (error) {
console.error('保存待办事项失败:', error);
dialog.showErrorBox('错误', '保存待办事项失败: ' + error.message);
return { success: false, error: error.message };
}
});
// IPC 处理:加载待办事项数据
ipcMain.handle('load-todos', async (event) => {
try {
if (fs.existsSync(dataFilePath)) {
const data = fs.readFileSync(dataFilePath, 'utf-8');
return { success: true, todos: JSON.parse(data) };
}
return { success: true, todos: [] }; // 如果文件不存在,返回空数组
} catch (error) {
console.error('加载待办事项失败:', error);
dialog.showErrorBox('错误', '加载待办事项失败: ' + error.message);
return { success: false, error: error.message };
}
});
预加载脚本将负责安全地将 IPC 功能暴露给渲染进程。
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('todoAPI', {
saveTodos: (todos) => ipcRenderer.invoke('save-todos', todos),
loadTodos: () => ipcRenderer.invoke('load-todos')
});
简单的 HTML 结构,包含输入框、按钮和列表。
我的待办事项
待办事项列表
渲染进程将处理 UI 交互和通过 `window.todoAPI` 与主进程通信。
// renderer.js
const todoInput = document.getElementById('new-todo-input');
const addTodoButton = document.getElementById('add-todo-button');
const todoList = document.getElementById('todo-list');
let todos = []; // 存储待办事项的数组
// 函数:渲染待办事项列表
function renderTodos() {
todoList.innerHTML = ''; // 清空现有列表
todos.forEach((todo, index) => {
const li = document.createElement('li');
li.className = todo.completed ? 'completed' : '';
li.innerHTML = `
${escapeHtml(todo.text)}
`;
todoList.appendChild(li);
});
}
// 简单的 HTML 转义函数,防止 XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.innerText = text;
return div.innerHTML;
}
// 函数:保存待办事项到文件
async function saveTodosToFile() {
const result = await window.todoAPI.saveTodos(todos);
if (!result.success) {
console.error('保存失败:', result.error);
// 可以在这里向用户显示一个错误通知
}
}
// 事件监听:添加待办
async function addTodo() {
const text = todoInput.value.trim();
if (text) {
todos.push({ text: text, completed: false });
todoInput.value = ''; // 清空输入框
renderTodos();
await saveTodosToFile(); // 保存到文件
}
}
addTodoButton.addEventListener('click', addTodo);
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTodo();
}
});
// 事件监听:切换完成状态和删除
todoList.addEventListener('click', async (event) => {
const button = event.target.closest('button');
if (!button) return;
const index = parseInt(button.dataset.index, 10);
if (isNaN(index) || index < 0 || index >= todos.length) {
return;
}
if (button.classList.contains('toggle')) {
todos[index].completed = !todos[index].completed;
} else if (button.classList.contains('delete')) {
todos.splice(index, 1); // 删除对应项
}
renderTodos();
await saveTodosToFile();
});
// 页面加载时加载待办事项
document.addEventListener('DOMContentLoaded', async () => {
const result = await window.todoAPI.loadTodos();
if (result.success && result.todos) {
todos = result.todos;
renderTodos();
} else {
console.error('加载失败:', result.error);
// 可以在这里向用户显示一个错误通知
}
});
按照第二部分的"应用程序打包与分发"中的说明,使用 `electron-builder` 配置 `package.json` 并运行 `npm run dist` 进行打包。
打包完成后,在 `dist` 目录下找到您的安装包或可执行文件,安装并运行它,测试您的待办事项应用是否正常工作,包括添加、标记完成、删除以及最重要的——关闭应用后重新打开,数据是否依然存在。
恭喜您!您已经完成了您的第一个 Electron 桌面应用程序的开发,并学习了从入门到高级的核心概念和实践。
Electron 的世界广阔而充满可能,希望本教程能为您打开探索的大门。祝您在桌面应用开发的道路上越走越远!
如果您在学习过程中有任何疑问,或者想了解更深入的特定主题,请随时提出。