从入门到精通的完整指南
本教程假设您已经具备 HTML、CSS 和 JavaScript 的基础知识。如果您是前端开发人员,那么恭喜您,Electron 将让您如鱼得水!
想象一下,您用 HTML 写了一个漂亮的网页,用 CSS 美化了它,再用 JavaScript 赋予了它各种交互功能。现在,您想把这个"网页"变成一个独立运行在 Windows、macOS 或 Linux 系统上的桌面程序,就像 QQ、VS Code 或者 Slack 那样。Electron 就是实现这个梦想的神奇工具!
Electron 是由 GitHub 开发的一个开源框架,它允许开发者使用Web 技术(HTML、CSS 和 JavaScript)构建原生的跨平台桌面应用程序。它通过将 Chromium(Google Chrome 浏览器核心)和 Node.js(JavaScript 运行时)集成到一个软件包中来实现这一点。
这是理解 Electron 最最核心的两个概念。可以把它们想象成一场演出的两位主角:
渲染进程不能直接访问 Node.js API!如果渲染进程需要进行文件读写等操作,它必须通过进程间通信 (IPC) 的方式,请求主进程代为执行。
开始之前,我们需要准备好开发环境。
Electron 基于 Node.js 运行,并使用 npm 进行包管理。如果您尚未安装,请前往 Node.js 官网 下载并安装 LTS 版本。
node -v
npm -v
mkdir my-electron-app
cd my-electron-app
npm init -y
npm install --save-dev electron
一个基本的 Electron 应用需要三个核心文件:
主进程脚本
渲染进程界面
安全桥梁脚本
main.js (主进程):
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
index.html (界面):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的第一个 Electron 应用</title>
<style>
body {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
h1 { font-size: 3em; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
</style>
</head>
<body>
<h1>Hello, Electron World! 👋</h1>
</body>
</html>
preload.js (预加载脚本):
// 当前暂时留空,后续会详细讲解
// 这个脚本是主进程和渲染进程之间的安全桥梁
在 package.json 中添加启动脚本:
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^latest"
}
}
npm start
如果一切顺利,您应该看到一个漂亮的窗口弹出。您的第一个 Electron 应用成功运行了!
主进程是 Electron 应用的"大脑",它拥有访问操作系统底层 API 的能力。
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 600,
minHeight: 400,
titleBarStyle: 'hiddenInset', // macOS 样式
frame: true,
transparent: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js')
}
});
// 窗口事件监听
mainWindow.on('ready-to-show', () => {
mainWindow.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
IPC 是 Electron 架构的核心,实现主进程与渲染进程之间的安全通信。
在 Electron 12+ 中,默认启用 contextIsolation: true 和 nodeIntegration: false。必须通过预加载脚本安全地暴露 API。
preload.js (安全桥梁):
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content) => ipcRenderer.invoke('dialog:saveFile', content),
// 系统信息
getVersion: () => ipcRenderer.invoke('app:getVersion'),
// 通知
showNotification: (message) => ipcRenderer.invoke('notification:show', message)
});
main.js (主进程处理):
const { ipcMain, dialog, app, Notification } = require('electron');
const fs = require('fs');
// 文件对话框
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!canceled && filePaths.length > 0) {
const content = fs.readFileSync(filePaths[0], 'utf-8');
return { success: true, content, path: filePaths[0] };
}
return { success: false };
});
// 获取应用版本
ipcMain.handle('app:getVersion', () => {
return app.getVersion();
});
// 显示通知
ipcMain.handle('notification:show', (event, message) => {
new Notification({ title: '应用通知', body: message }).show();
return { success: true };
});
renderer.js (渲染进程使用):
// 使用预加载脚本暴露的 API
document.addEventListener('DOMContentLoaded', async () => {
// 获取应用版本
const version = await window.electronAPI.getVersion();
console.log('应用版本:', version);
// 打开文件
const openBtn = document.getElementById('openFile');
openBtn.addEventListener('click', async () => {
const result = await window.electronAPI.openFile();
if (result.success) {
document.getElementById('content').textContent = result.content;
}
});
// 显示通知
const notifyBtn = document.getElementById('notify');
notifyBtn.addEventListener('click', async () => {
await window.electronAPI.showNotification('Hello from Electron!');
});
});
桌面应用经常需要处理用户数据和文件。Electron 提供了多种存储选项。
简单键值对存储
Node.js fs 模块
SQLite 集成
// main.js 中的配置管理
const { app, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const configPath = path.join(app.getPath('userData'), 'config.json');
// 加载配置
ipcMain.handle('config:load', async () => {
try {
if (fs.existsSync(configPath)) {
const data = fs.readFileSync(configPath, 'utf-8');
return { success: true, config: JSON.parse(data) };
}
return { success: true, config: getDefaultConfig() };
} catch (error) {
return { success: false, error: error.message };
}
});
// 保存配置
ipcMain.handle('config:save', async (event, config) => {
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
function getDefaultConfig() {
return {
theme: 'light',
notifications: true,
autoSave: false
};
}
const { Menu, app } = require('electron');
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: async () => {
// 创建新文档逻辑
}
},
{ type: 'separator' },
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: async () => {
// 打开文件逻辑
}
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: async () => {
// 保存文件逻辑
}
}
]
},
{
label: '编辑',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
},
{
label: '视图',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
使用 electron-updater 实现自动更新功能:
npm install electron-updater
// main.js 中的自动更新
const { autoUpdater } = require('electron-updater');
// 配置更新
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
// 检查更新
app.whenReady().then(() => {
createWindow();
// 启动后检查更新
setTimeout(() => {
autoUpdater.checkForUpdatesAndNotify();
}, 2000);
});
// 更新事件
autoUpdater.on('update-available', (info) => {
dialog.showMessageBox({
type: 'info',
title: '发现新版本',
message: `发现新版本 ${info.version},是否现在下载?`,
buttons: ['下载', '取消']
}).then(result => {
if (result.response === 0) {
autoUpdater.downloadUpdate();
}
});
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
type: 'info',
title: '更新准备完成',
message: '新版本已下载,应用将重启以安装更新。',
buttons: ['立即重启', '稍后']
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
// 正确的事件监听器管理
class WindowManager {
constructor() {
this.windows = new Set();
this.eventListeners = new Map();
}
createWindow() {
const window = new BrowserWindow({...});
this.windows.add(window);
// 绑定清理函数
const cleanup = () => {
this.windows.delete(window);
this.cleanupEventListeners(window);
};
window.on('closed', cleanup);
this.eventListeners.set(window, cleanup);
return window;
}
cleanupEventListeners(window) {
const cleanup = this.eventListeners.get(window);
if (cleanup) {
cleanup();
this.eventListeners.delete(window);
}
}
closeAllWindows() {
this.windows.forEach(window => {
if (!window.isDestroyed()) {
window.close();
}
});
}
}
永远不要将不可信的外部内容加载到开启了 Node.js 集成的窗口中!
nodeIntegration: false
contextIsolation: true
enableRemoteModule: false
我们将构建一个功能完整的待办事项应用,整合前面学到的所有知识:
todo-electron-app/
├── src/
│ ├── main.js # 主进程
│ ├── preload.js # 预加载脚本
│ └── renderer/
│ ├── index.html # 主界面
│ ├── style.css # 样式文件
│ └── app.js # 渲染进程逻辑
├── package.json
└── README.md
main.js (主进程):
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow;
const dataPath = path.join(app.getPath('userData'), 'todos.json');
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 700,
minWidth: 600,
minHeight: 400,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hiddenInset',
show: false
});
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
}
// IPC 处理程序
ipcMain.handle('todos:load', async () => {
try {
if (fs.existsSync(dataPath)) {
const data = fs.readFileSync(dataPath, 'utf-8');
return JSON.parse(data);
}
return [];
} catch (error) {
console.error('加载待办事项失败:', error);
return [];
}
});
ipcMain.handle('todos:save', async (event, todos) => {
try {
fs.writeFileSync(dataPath, JSON.stringify(todos, null, 2));
return { success: true };
} catch (error) {
console.error('保存待办事项失败:', error);
return { success: false, error: error.message };
}
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
preload.js (安全桥梁):
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('todoAPI', {
loadTodos: () => ipcRenderer.invoke('todos:load'),
saveTodos: (todos) => ipcRenderer.invoke('todos:save', todos)
});
index.html (现代化界面):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header class="header">
<h1>📝 我的待办事项</h1>
<div class="stats" id="stats"></div>
</header>
<main class="main">
<form class="add-form" id="addForm">
<input
type="text"
id="todoInput"
placeholder="添加新的待办事项..."
required
>
<button type="submit">添加</button>
</form>
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">全部</button>
<button class="filter-tab" data-filter="active">未完成</button>
<button class="filter-tab" data-filter="completed">已完成</button>
</div>
<ul class="todo-list" id="todoList"></ul>
</main>
</div>
<script src="app.js"></script>
</body>
</html>
app.js (渲染进程逻辑):
class TodoApp {
constructor() {
this.todos = [];
this.currentFilter = 'all';
this.init();
}
async init() {
this.bindEvents();
await this.loadTodos();
this.render();
}
bindEvents() {
document.getElementById('addForm').addEventListener('submit', (e) => {
e.preventDefault();
this.addTodo();
});
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
this.setFilter(e.target.dataset.filter);
});
});
}
async addTodo() {
const input = document.getElementById('todoInput');
const text = input.value.trim();
if (text) {
const todo = {
id: Date.now(),
text: text,
completed: false,
createdAt: new Date().toISOString()
};
this.todos.push(todo);
input.value = '';
await this.saveTodos();
this.render();
}
}
async toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
await this.saveTodos();
this.render();
}
}
async deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
await this.saveTodos();
this.render();
}
setFilter(filter) {
this.currentFilter = filter;
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.filter === filter);
});
this.render();
}
getFilteredTodos() {
switch (this.currentFilter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
render() {
const todoList = document.getElementById('todoList');
const filteredTodos = this.getFilteredTodos();
todoList.innerHTML = filteredTodos.map(todo => `
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<input
type="checkbox"
${todo.completed ? 'checked' : ''}
onchange="app.toggleTodo(${todo.id})"
>
<span class="todo-text">${todo.text}</span>
<button class="delete-btn" onclick="app.deleteTodo(${todo.id})">
🗑️
</button>
</li>
`).join('');
this.updateStats();
}
updateStats() {
const total = this.todos.length;
const completed = this.todos.filter(t => t.completed).length;
const remaining = total - completed;
document.getElementById('stats').textContent =
`总计: ${total} | 已完成: ${completed} | 剩余: ${remaining}`;
}
async loadTodos() {
try {
this.todos = await window.todoAPI.loadTodos();
} catch (error) {
console.error('加载待办事项失败:', error);
this.todos = [];
}
}
async saveTodos() {
try {
await window.todoAPI.saveTodos(this.todos);
} catch (error) {
console.error('保存待办事项失败:', error);
}
}
}
const app = new TodoApp();
使用 electron-builder 进行应用打包:
# 安装打包工具
npm install --save-dev electron-builder
# 添加打包脚本到 package.json
npm run dist
package.json 配置示例:
{
"name": "todo-electron-app",
"version": "1.0.0",
"main": "src/main.js",
"scripts": {
"start": "electron .",
"dist": "electron-builder",
"dist:win": "electron-builder --win",
"dist:mac": "electron-builder --mac",
"dist:linux": "electron-builder --linux"
},
"build": {
"appId": "com.example.todoapp",
"productName": "Todo应用",
"directories": {
"output": "dist"
},
"files": [
"src/**/*",
"package.json"
],
"mac": {
"icon": "assets/icon.icns",
"category": "public.app-category.productivity"
},
"win": {
"icon": "assets/icon.ico",
"target": ["nsis", "portable"]
},
"linux": {
"icon": "assets/icon.png",
"target": ["AppImage", "deb"]
}
},
"devDependencies": {
"electron": "^latest",
"electron-builder": "^latest"
}
}
您已经掌握了 Electron 桌面应用开发的核心技能!
进程架构、IPC通信、数据存储
完整的待办事项应用
探索更多高级特性