第一个 Electron 应用
本文将带你从零开始创建一个完整的 Electron 应用,深入了解 Electron 应用的工作原理和最佳实践。
前置要求
在开始之前,确保你已经安装了以下工具:
- Node.js: 建议使用 LTS 版本(18.x 或更高)
- npm 或 pnpm: 包管理器
- 代码编辑器: 推荐 VS Code
💡 如果你还未配置开发环境,请先参考 简介与安装 完成环境配置
项目初始化
1. 创建项目结构
bash
# 创建项目目录
mkdir my-electron-app
cd my-electron-app
# 初始化 package.json
npm init -y2. 安装 Electron
bash
npm install electron --save-dev3. 配置 package.json
修改 package.json 文件,设置入口文件和启动脚本:
json
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "我的第一个 Electron 应用",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --enable-logging"
},
"keywords": ["electron", "desktop", "app"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0"
}
}关键配置说明:
main: 指定 Electron 应用的主进程入口文件scripts.start: 启动应用的快捷命令electron作为devDependencies: 打包后的应用会包含 Electron 二进制文件,运行时不需要单独依赖
核心文件编写
1. 主进程文件 (main.js)
主进程是 Electron 应用的核心,负责创建窗口、管理应用生命周期、处理系统交互。
javascript
// main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
/**
* 创建浏览器窗口
* BrowserWindow 是 Electron 提供的窗口管理类
*/
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800, // 窗口宽度
height: 600, // 窗口高度
webPreferences: {
// 预加载脚本路径
// path.join 使用跨平台路径拼接,避免 Windows/macOS 路径分隔符差异
preload: path.join(__dirname, 'preload.js'),
// 启用上下文隔离(安全最佳实践,Electron 12+ 默认启用)
contextIsolation: true,
// 禁用 Node.js 集成(安全最佳实践,推荐禁用)
nodeIntegration: false,
// 启用沙箱模式(增强安全性)
sandbox: true
}
})
// 加载应用的 HTML 文件
mainWindow.loadFile('index.html')
// 开发环境自动打开开发者工具
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools()
}
// 窗口关闭时的事件处理
mainWindow.on('closed', () => {
// 解除窗口引用,帮助垃圾回收
mainWindow = null
})
}
// Electron 初始化完成后创建窗口
// app.whenReady() 返回一个 Promise,确保 app 完全就绪
app.whenReady().then(() => {
createWindow()
// macOS 特殊处理
// 在 macOS 上,当点击 dock 图标且没有其他窗口打开时,
// 通常会在应用程序中重新创建一个窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
// 关闭所有窗口时退出应用(macOS 除外)
// macOS 应用通常在用户明确退出前保持活动状态
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// 在应用退出前清理资源
app.on('before-quit', () => {
console.log('应用即将退出')
})2. 预加载脚本 (preload.js)
预加载脚本运行在渲染进程中,但拥有访问 Node.js API 的能力,是连接主进程和渲染进程的安全桥梁。
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
/**
* 使用 contextBridge 安全地将 API 暴露给渲染进程
* contextIsolation: true 时必须使用此方法
*
* 优势:
* 1. 防止原型污染攻击
* 2. 确保只有经过验证的 API 可以在渲染进程中使用
* 3. 提供类型安全的接口
*/
contextBridge.exposeInMainWorld('electronAPI', {
// 获取版本信息
versions: {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
},
// IPC 通信示例:发送消息到主进程
send: (channel, data) => {
// 白名单验证,防止任意 IPC 调用
const validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
// IPC 通信示例:接收主进程消息
receive: (channel, func) => {
const validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// 移除旧的监听器,避免内存泄漏
ipcRenderer.removeAllListeners(channel)
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
},
// IPC 通信示例:双向通信
invoke: async (channel, data) => {
const validChannels = ['dialog:open', 'file:read']
if (validChannels.includes(channel)) {
return await ipcRenderer.invoke(channel, data)
}
throw new Error(`Invalid IPC channel: ${channel}`)
}
})3. HTML 页面 (index.html)
渲染进程的 HTML 页面,用于展示应用界面。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Content Security Policy: 防止 XSS 攻击 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<title>我的第一个 Electron 应用</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
}
h1 {
font-size: 3rem;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.info {
background: rgba(255, 255, 255, 0.1);
padding: 20px 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
margin-top: 20px;
}
.info p {
margin: 10px 0;
font-size: 1.1rem;
}
.version {
color: #ffd700;
font-weight: bold;
}
</style>
</head>
<body>
<h1>🎉 Hello Electron!</h1>
<p>欢迎使用 Electron 桌面应用开发</p>
<div class="info">
<p>Node.js 版本: <span class="version" id="node-version"></span></p>
<p>Chromium 版本: <span class="version" id="chrome-version"></span></p>
<p>Electron 版本: <span class="version" id="electron-version"></span></p>
</div>
<!-- 引入渲染进程脚本 -->
<script src="./renderer.js"></script>
</body>
</html>4. 渲染进程脚本 (renderer.js)
在渲染进程中运行的 JavaScript 代码,处理页面交互和显示逻辑。
javascript
// renderer.js
/**
* 使用 preload.js 暴露的 electronAPI
* window.electronAPI 是通过 contextBridge 注入的安全接口
*/
function displayVersions() {
try {
// 获取并显示版本信息
const nodeVersion = window.electronAPI.versions.node()
const chromeVersion = window.electronAPI.versions.chrome()
const electronVersion = window.electronAPI.versions.electron()
document.getElementById('node-version').innerText = nodeVersion
document.getElementById('chrome-version').innerText = chromeVersion
document.getElementById('electron-version').innerText = electronVersion
console.log('版本信息加载成功')
} catch (error) {
console.error('获取版本信息失败:', error)
}
}
// DOM 加载完成后执行
window.addEventListener('DOMContentLoaded', () => {
displayVersions()
console.log('渲染进程已就绪')
})运行应用
启动开发环境
bash
npm start或使用开发模式(带日志输出):
bash
npm run dev启动后,你应该能看到一个窗口显示版本信息。
项目结构说明
完成后的项目结构如下:
code
my-electron-app/
├── package.json # 项目配置和依赖管理
├── main.js # 主进程入口文件
├── preload.js # 预加载脚本(安全桥梁)
├── index.html # 渲染进程 HTML 页面
├── renderer.js # 渲染进程 JavaScript
├── .gitignore # Git 忽略文件配置
└── node_modules/ # 项目依赖(自动生成)文件职责
| 文件 | 进程类型 | 主要职责 |
|---|---|---|
main.js | 主进程 | 创建窗口、管理应用生命周期、系统交互 |
preload.js | 渲染进程 | 安全暴露 Node.js API 给渲染进程 |
index.html | 渲染进程 | 应用界面结构 |
renderer.js | 渲染进程 | 页面交互逻辑、DOM 操作 |
调试方法
1. 使用 Chrome DevTools
Electron 内置了 Chrome DevTools,方便调试渲染进程:
方法一:代码自动打开
javascript
// main.js
mainWindow.webContents.openDevTools()方法二:快捷键
- macOS:
Cmd + Option + I - Windows/Linux:
F12或Ctrl + Shift + I
方法三:菜单栏
在应用菜单中选择 View → Toggle Developer Tools
2. 主进程调试
方法一:VS Code 调试配置
在项目根目录创建 .vscode/launch.json:
json
{
"version": "0.2.0",
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"],
"stopAll": true
}
],
"configurations": [
{
"name": "Electron: Main",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": ["."],
"outputCapture": "std",
"console": "integratedTerminal"
},
{
"name": "Electron: Renderer",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}",
"timeout": 30000
}
]
}使用方法:
- 按
F5或点击 VS Code 调试面板的"运行"按钮 - 选择
Electron: All同时调试主进程和渲染进程 - 在代码中设置断点即可
方法二:命令行调试
bash
# 启用 Node.js 调试模式
electron --inspect=5858 .
electron --inspect-brk=5858 . # 在第一行代码处暂停然后在 Chrome 浏览器打开 chrome://inspect 连接调试。
3. 日志调试
javascript
// main.js - 主进程日志
console.log('主进程日志:', data)
// renderer.js - 渲染进程日志
console.log('渲染进程日志:', data)
// 查看 IPC 通信
ipcMain.on('channel', (event, data) => {
console.log('收到 IPC 消息:', data)
})查看日志位置:
- 主进程日志: 终端输出
- 渲染进程日志: Chrome DevTools Console
常见错误与解决方案
1. 模块未找到错误
错误信息:
code
Error: Cannot find module 'electron'解决方案:
bash
# 确认 electron 已安装
npm ls electron
# 重新安装
npm install electron --save-dev2. 窗口无法显示
可能原因及解决方案:
javascript
// 检查 1: 确认 HTML 文件路径正确
mainWindow.loadFile('index.html') // 确保文件存在
// 检查 2: 捕获加载错误
mainWindow.loadFile('index.html').catch(err => {
console.error('页面加载失败:', err)
})
// 检查 3: 监听加载事件
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('加载失败:', errorCode, errorDescription)
})3. contextBridge 报错
错误信息:
code
contextBridge is not defined解决方案:
确保在 webPreferences 中正确配置:
javascript
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必须启用
nodeIntegration: false // 必须禁用
}
})4. IPC 通信失败
错误信息:
code
Error: An object could not be cloned原因: IPC 消息只能传递可序列化的数据(JSON 支持的类型)
解决方案:
javascript
// ❌ 错误:传递函数或 DOM 元素
ipcRenderer.send('channel', { callback: () => {} })
// ✅ 正确:只传递可序列化数据
ipcRenderer.send('channel', {
id: 1,
name: 'test',
data: { key: 'value' }
})5. 空白页面问题
可能原因:
- CSP 策略限制:
html
<!-- 检查 CSP 设置 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">- 文件路径错误:
javascript
// 使用 path.join 确保路径正确
mainWindow.loadFile(path.join(__dirname, 'index.html'))- JavaScript 错误: 打开 DevTools 查看控制台是否有错误信息。
6. macOS 窗口关闭后应用不退出
这是 macOS 的正常行为,需添加处理:
javascript
app.on('window-all-closed', () => {
// macOS 应用通常在用户明确退出前保持活动状态
if (process.platform !== 'darwin') {
app.quit()
}
})最佳实践
1. 安全最佳实践
javascript
// ✅ 推荐
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true, // 启用上下文隔离
nodeIntegration: false, // 禁用 Node.js 集成
sandbox: true, // 启用沙箱
preload: path.join(__dirname, 'preload.js')
}
})
// ❌ 不安全(仅用于开发测试)
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true, // 危险!
contextIsolation: false // 危险!
}
})2. 窗口管理
javascript
// 存储窗口引用,避免被垃圾回收
let mainWindow = null
function createWindow() {
mainWindow = new BrowserWindow({...})
mainWindow.on('closed', () => {
mainWindow = null // 解除引用
})
}
// 多窗口管理
const windows = new Set()
function createWindow() {
const win = new BrowserWindow({...})
windows.add(win)
win.on('closed', () => windows.delete(win))
}3. 环境变量管理
javascript
// 使用 dotenv 管理环境变量
require('dotenv').config()
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
if (isDev) {
mainWindow.webContents.openDevTools()
}4. 错误处理
javascript
// 全局错误捕获
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error)
// 可以在这里添加错误上报
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason)
})
// 窗口加载错误
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('页面加载失败:', errorCode, errorDescription)
})5. 性能优化
javascript
// 延迟加载模块
let heavyModule = null
async function loadHeavyModule() {
if (!heavyModule) {
heavyModule = await import('./heavy-module.js')
}
return heavyModule
}
// 节流和防抖
const { debounce } = require('lodash')
const debouncedSave = debounce((data) => {
// 保存数据
}, 300)
// 及时清理资源
mainWindow.on('closed', () => {
mainWindow.webContents.session.clearCache()
mainWindow = null
})6. 代码组织
推荐的项目结构:
code
my-electron-app/
├── src/
│ ├── main/ # 主进程代码
│ │ ├── index.js # 入口文件
│ │ ├── window.js # 窗口管理
│ │ └── ipc.js # IPC 通信
│ ├── renderer/ # 渲染进程代码
│ │ ├── index.html
│ │ ├── renderer.js
│ │ └── styles/
│ └── preload/ # 预加载脚本
│ └── index.js
├── resources/ # 应用资源
│ ├── icon.png
│ └── tray-icon.png
├── tests/ # 测试文件
├── package.json
└── README.md下一步学习
- 项目结构说明 - 了解 Electron 项目的标准结构和文件组织方式
- 核心概念 - 深入学习主进程、渲染进程、IPC 通信等核心概念
- API 参考 - 探索更多 Electron API 功能
- 打包发布 - 学习如何打包和分发你的应用