Electron 学习指南
什么是 Electron
Electron 是使用 Web 技术构建跨平台桌面应用的框架。
核心特点
- 跨平台:Windows、macOS、Linux
- Web 技术:HTML、CSS、JavaScript
- Node.js:完整的 Node.js 环境
- 原生能力:系统 API 访问
快速开始
安装
mkdir my-electron-app
cd my-electron-app
yarn init -y
yarn add -D electron
创建主进程
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
win.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>Hello Electron</title>
</head>
<body>
<h1>Hello Electron!</h1>
<script src="renderer.js"></script>
</body>
</html>
配置 package.json
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
运行
yarn start
主进程与渲染进程
主进程
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
ipcMain.handle('get-data', async () => {
return { message: 'Hello from main process' };
});
预加载脚本
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getData: () => ipcRenderer.invoke('get-data')
});
渲染进程
// renderer.js
async function loadData() {
const data = await window.electronAPI.getData();
console.log(data);
}
loadData();
窗口管理
const { BrowserWindow } = require('electron');
// 创建窗口
const win = new BrowserWindow({
width: 800,
height: 600,
title: '我的应用',
icon: 'icon.png',
resizable: true,
minimizable: true,
maximizable: true,
frame: true,
transparent: false,
alwaysOnTop: false
});
// 加载页面
win.loadFile('index.html');
win.loadURL('https://example.com');
// 窗口事件
win.on('ready-to-show', () => {
win.show();
});
win.on('closed', () => {
console.log('窗口关闭');
});
菜单
const { Menu } = require('electron');
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => {
console.log('新建');
}
},
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
对话框
const { dialog } = require('electron');
// 打开文件
const result = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif'] },
{ name: 'All Files', extensions: ['*'] }
]
});
// 保存文件
const savePath = await dialog.showSaveDialog({
defaultPath: 'untitled.txt'
});
// 消息框
dialog.showMessageBox({
type: 'info',
title: '提示',
message: '操作成功',
buttons: ['确定']
});
学习路径
- 基础概念 → 主进程、渲染进程
- 进程通信 → IPC
- 窗口管理 → BrowserWindow
- 菜单和托盘 → Menu、Tray
- 文件系统 → dialog、fs
- 打包发布 → electron-builder
- 自动更新 → electron-updater
- 性能优化 → 内存管理、启动优化
完整示例:记事本应用
项目结构
notepad-app/
├── main.js # 主进程
├── preload.js # 预加载脚本
├── index.html # 渲染进程
├── renderer.js # 渲染进程逻辑
├── styles.css # 样式
└── package.json # 配置
main.js
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const path = require('path');
const fs = require('fs').promises;
let mainWindow;
let currentFilePath = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
mainWindow.loadFile('index.html');
createMenu();
}
function createMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => {
mainWindow.webContents.send('file-new');
}
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0];
const content = await fs.readFile(filePath, 'utf-8');
currentFilePath = filePath;
mainWindow.webContents.send('file-opened', { filePath, content });
}
}
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => {
mainWindow.webContents.send('file-save');
}
},
{
label: '另存为',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => {
mainWindow.webContents.send('file-save-as');
}
},
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
{ role: 'selectAll', label: '全选' }
]
},
{
label: '查看',
submenu: [
{ role: 'reload', label: '重新加载' },
{ role: 'toggleDevTools', label: '开发者工具' },
{ type: 'separator' },
{ role: 'resetZoom', label: '实际大小' },
{ role: 'zoomIn', label: '放大' },
{ role: 'zoomOut', label: '缩小' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// 保存文件
ipcMain.handle('save-file', async (event, { filePath, content }) => {
try {
let saveFilePath = filePath || currentFilePath;
if (!saveFilePath) {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (result.canceled) {
return { success: false, canceled: true };
}
saveFilePath = result.filePath;
}
await fs.writeFile(saveFilePath, content, 'utf-8');
currentFilePath = saveFilePath;
return { success: true, filePath: saveFilePath };
} catch (error) {
return { success: false, error: error.message };
}
});
// 另存为
ipcMain.handle('save-file-as', async (event, content) => {
try {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (result.canceled) {
return { success: false, canceled: true };
}
await fs.writeFile(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
return { success: true, filePath: result.filePath };
} catch (error) {
return { success: false, error: error.message };
}
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (filePath, content) => ipcRenderer.invoke('save-file', { filePath, content }),
saveFileAs: (content) => ipcRenderer.invoke('save-file-as', content),
onFileNew: (callback) => ipcRenderer.on('file-new', callback),
onFileOpened: (callback) => ipcRenderer.on('file-opened', (event, data) => callback(data)),
onFileSave: (callback) => ipcRenderer.on('file-save', callback),
onFileSaveAs: (callback) => ipcRenderer.on('file-save-as', callback)
});
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>记事本</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="file-info">
<span id="fileName">未命名</span>
<span id="modified" class="modified hidden">●</span>
</div>
<div class="stats">
<span id="charCount">0 字符</span>
<span id="lineCount">1 行</span>
</div>
</div>
<textarea id="editor" placeholder="开始输入..."></textarea>
</div>
<script src="renderer.js"></script>
</body>
</html>
renderer.js
const editor = document.getElementById('editor');
const fileName = document.getElementById('fileName');
const modified = document.getElementById('modified');
const charCount = document.getElementById('charCount');
const lineCount = document.getElementById('lineCount');
let currentFilePath = null;
let isModified = false;
let originalContent = '';
// 更新统计信息
function updateStats() {
const content = editor.value;
const chars = content.length;
const lines = content.split('\n').length;
charCount.textContent = `${chars} 字符`;
lineCount.textContent = `${lines} 行`;
}
// 标记为已修改
function markAsModified() {
if (editor.value !== originalContent) {
isModified = true;
modified.classList.remove('hidden');
} else {
isModified = false;
modified.classList.add('hidden');
}
}
// 编辑器输入事件
editor.addEventListener('input', () => {
updateStats();
markAsModified();
});
// 新建文件
window.electronAPI.onFileNew(() => {
if (isModified) {
if (!confirm('当前文件未保存,确定要新建吗?')) {
return;
}
}
editor.value = '';
currentFilePath = null;
originalContent = '';
isModified = false;
fileName.textContent = '未命名';
modified.classList.add('hidden');
updateStats();
});
// 打开文件
window.electronAPI.onFileOpened((data) => {
editor.value = data.content;
currentFilePath = data.filePath;
originalContent = data.content;
isModified = false;
fileName.textContent = data.filePath.split('/').pop();
modified.classList.add('hidden');
updateStats();
});
// 保存文件
window.electronAPI.onFileSave(async () => {
const result = await window.electronAPI.saveFile(currentFilePath, editor.value);
if (result.success) {
currentFilePath = result.filePath;
originalContent = editor.value;
isModified = false;
fileName.textContent = result.filePath.split('/').pop();
modified.classList.add('hidden');
} else if (!result.canceled) {
alert('保存失败: ' + result.error);
}
});
// 另存为
window.electronAPI.onFileSaveAs(async () => {
const result = await window.electronAPI.saveFileAs(editor.value);
if (result.success) {
currentFilePath = result.filePath;
originalContent = editor.value;
isModified = false;
fileName.textContent = result.filePath.split('/').pop();
modified.classList.add('hidden');
} else if (!result.canceled) {
alert('保存失败: ' + result.error);
}
});
// 初始化
updateStats();
styles.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
}
#fileName {
font-weight: 500;
font-size: 14px;
}
.modified {
color: #007AFF;
font-size: 20px;
}
.modified.hidden {
display: none;
}
.stats {
display: flex;
gap: 20px;
font-size: 12px;
color: #666;
}
#editor {
flex: 1;
padding: 20px;
border: none;
outline: none;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
}
#editor::placeholder {
color: #999;
}
package.json
{
"name": "notepad-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.0.0"
},
"build": {
"appId": "com.example.notepad",
"productName": "记事本",
"directories": {
"output": "dist"
},
"files": [
"**/*",
"!node_modules/**/*",
"node_modules/electron/**/*"
],
"mac": {
"target": ["dmg"],
"icon": "build/icon.icns"
},
"win": {
"target": ["nsis"],
"icon": "build/icon.ico"
}
}
}
实战:Todo 应用
使用 SQLite 数据库
yarn add better-sqlite3
main.js (数据库部分)
const Database = require('better-sqlite3');
const path = require('path');
// 初始化数据库
const db = new Database(path.join(app.getPath('userData'), 'todos.db'));
// 创建表
db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 获取所有待办
ipcMain.handle('get-todos', () => {
const stmt = db.prepare('SELECT * FROM todos ORDER BY created_at DESC');
return stmt.all();
});
// 添加待办
ipcMain.handle('add-todo', (event, text) => {
const stmt = db.prepare('INSERT INTO todos (text) VALUES (?)');
const result = stmt.run(text);
return { id: result.lastInsertRowid, text, completed: 0 };
});
// 切换完成状态
ipcMain.handle('toggle-todo', (event, id) => {
const stmt = db.prepare('UPDATE todos SET completed = NOT completed WHERE id = ?');
stmt.run(id);
return true;
});
// 删除待办
ipcMain.handle('delete-todo', (event, id) => {
const stmt = db.prepare('DELETE FROM todos WHERE id = ?');
stmt.run(id);
return true;
});
// 应用退出时关闭数据库
app.on('will-quit', () => {
db.close();
});
调试技巧
主进程调试
# 使用 VSCode 调试
# .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": ["."],
"outputCapture": "std"
}
]
}
渲染进程调试
// 开发环境自动打开 DevTools
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
日志输出
// 主进程
console.log('主进程日志');
// 渲染进程
console.log('渲染进程日志');
// 使用 electron-log
const log = require('electron-log');
log.info('应用启动');
log.error('发生错误');
性能优化
1. 延迟加载
// 延迟显示窗口
const win = new BrowserWindow({ show: false });
win.once('ready-to-show', () => {
win.show();
});
2. 预加载优化
// 只暴露必要的 API
contextBridge.exposeInMainWorld('api', {
// 只暴露需要的方法
saveFile: (data) => ipcRenderer.invoke('save-file', data)
});
3. 内存管理
// 及时清理不用的窗口
win.on('closed', () => {
win = null;
});
// 限制窗口数量
if (BrowserWindow.getAllWindows().length > 5) {
// 关闭旧窗口
}
学习路径
第一阶段:基础(1周)
- 主进程和渲染进程概念
- 创建窗口
- 加载页面
- 基础 IPC 通信
第二阶段:进阶(2周)
- 菜单和托盘
- 对话框
- 文件系统操作
- 数据库集成
第三阶段:实战(2周)
- 完整应用开发
- 打包配置
- 自动更新
- 性能优化
第四阶段:发布(1周)
- 代码签名
- 应用商店发布
- 持续集成
- 用户反馈收集
推荐资源
- Electron 官方文档
- Electron Fiddle - 在线实验
- Awesome Electron - 资源列表
- Electron Builder - 打包工具文档