Electron 快速开始 - 30 分钟创建第一个应用
前言
环境搭建完成后,现在让我们动手创建第一个 Electron 应用!这篇文章会带你从零开始,30 分钟内创建一个完整的桌面应用,包括窗口创建、进程通信、文件操作等基本功能。
我还记得第一次创建 Electron 应用时的兴奋感——用熟悉的 Web 技术,几十行代码就能创建一个真正的桌面应用,那种成就感真的很棒。
让我们开始吧!
创建项目
方式一:使用官方脚手架(推荐)
bash
# 创建项目
npm init electron-app@latest my-electron-app
# 进入项目目录
cd my-electron-app
# 启动应用
npm start方式二:从零开始手动创建
bash
# 创建项目目录
mkdir my-electron-app
cd my-electron-app
# 初始化 npm 项目
npm init -y
# 安装 Electron
npm install --save-dev electron
# 创建项目文件
touch main.js index.html preload.js项目结构详解
my-electron-app/
├── node_modules/ # 依赖包
├── main.js # 主进程入口
├── preload.js # 预加载脚本
├── index.html # 渲染进程页面
├── renderer.js # 渲染进程脚本
├── styles.css # 样式文件
└── package.json # 项目配置package.json 配置
json
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "My first Electron app",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --inspect"
},
"keywords": ["electron"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0"
}
}主进程实现
main.js - 基础版本
javascript
const { app, BrowserWindow } = require('electron')
const path = require('path')
// 创建浏览器窗口
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
// 加载 index.html
mainWindow.loadFile('index.html')
// 打开开发者工具(开发时)
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools()
}
}
// Electron 准备就绪时创建窗口
app.whenReady().then(() => {
createWindow()
// macOS 特定:点击 Dock 图标时重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
// 所有窗口关闭时退出应用(macOS 除外)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})添加 IPC 通信
javascript
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
mainWindow.loadFile('index.html')
}
// IPC 处理器
ipcMain.handle('get-app-version', () => {
return app.getVersion()
})
ipcMain.handle('get-system-info', () => {
return {
platform: process.platform,
arch: process.arch,
version: process.version,
electron: process.versions.electron,
chrome: process.versions.chrome,
node: process.versions.node
}
})
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
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 获取应用版本
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
// 获取系统信息
getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
// 发送消息到主进程
send: (channel, data) => {
const validChannels = ['message-to-main']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
// 接收来自主进程的消息
on: (channel, callback) => {
const validChannels = ['message-from-main']
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => callback(...args))
}
}
})渲染进程页面
index.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<title>My Electron App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>🎉 我的第一个 Electron 应用</h1>
<p class="subtitle">使用 Web 技术构建的桌面应用</p>
</header>
<main>
<!-- 系统信息卡片 -->
<div class="card">
<h2>📊 系统信息</h2>
<div id="system-info" class="info-grid">
<div class="info-item">
<span class="label">操作系统:</span>
<span id="platform" class="value">-</span>
</div>
<div class="info-item">
<span class="label">架构:</span>
<span id="arch" class="value">-</span>
</div>
<div class="info-item">
<span class="label">Electron:</span>
<span id="electron-version" class="value">-</span>
</div>
<div class="info-item">
<span class="label">Chrome:</span>
<span id="chrome-version" class="value">-</span>
</div>
<div class="info-item">
<span class="label">Node.js:</span>
<span id="node-version" class="value">-</span>
</div>
<div class="info-item">
<span class="label">应用版本:</span>
<span id="app-version" class="value">-</span>
</div>
</div>
</div>
<!-- 功能演示卡片 -->
<div class="card">
<h2>⚡ 功能演示</h2>
<div class="demo-section">
<h3>计数器</h3>
<div class="counter">
<button id="decrease">-</button>
<span id="count">0</span>
<button id="increase">+</button>
</div>
</div>
<div class="demo-section">
<h3>消息通知</h3>
<input type="text" id="message-input" placeholder="输入消息...">
<button id="send-message">发送</button>
<div id="messages"></div>
</div>
</div>
<!-- 学习资源卡片 -->
<div class="card">
<h2>📚 学习资源</h2>
<ul class="resource-list">
<li><a href="#" onclick="openExternal('https://www.electronjs.org/')">Electron 官方文档</a></li>
<li><a href="#" onclick="openExternal('https://github.com/electron/electron')">GitHub 仓库</a></li>
<li><a href="#" onclick="openExternal('https://www.electronjs.org/docs/latest/tutorial/quick-start')">快速开始教程</a></li>
</ul>
</div>
</main>
<footer>
<p>Made with ❤️ using Electron</p>
</footer>
</div>
<script src="renderer.js"></script>
</body>
</html>styles.css
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.2em;
opacity: 0.9;
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.card h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5em;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
}
.label {
font-weight: 600;
color: #495057;
}
.value {
color: #667eea;
font-family: 'Courier New', monospace;
}
.demo-section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
}
.demo-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.demo-section h3 {
color: #495057;
margin-bottom: 15px;
font-size: 1.1em;
}
.counter {
display: flex;
align-items: center;
gap: 20px;
justify-content: center;
}
.counter button {
width: 50px;
height: 50px;
font-size: 24px;
border: none;
border-radius: 50%;
background: #667eea;
color: white;
cursor: pointer;
transition: all 0.3s;
}
.counter button:hover {
background: #764ba2;
transform: scale(1.1);
}
.counter span {
font-size: 32px;
font-weight: bold;
color: #667eea;
min-width: 60px;
text-align: center;
}
input[type="text"] {
width: calc(100% - 80px);
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
margin-right: 10px;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
#messages {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
min-height: 60px;
max-height: 200px;
overflow-y: auto;
}
.message-item {
padding: 8px 12px;
background: white;
border-left: 3px solid #667eea;
margin-bottom: 8px;
border-radius: 4px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.resource-list {
list-style: none;
}
.resource-list li {
padding: 12px;
margin-bottom: 8px;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s;
}
.resource-list li:hover {
background: #667eea;
transform: translateX(10px);
}
.resource-list a {
color: #495057;
text-decoration: none;
display: block;
}
.resource-list li:hover a {
color: white;
}
footer {
text-align: center;
color: white;
margin-top: 30px;
padding: 20px;
opacity: 0.8;
}renderer.js
javascript
// 加载系统信息
async function loadSystemInfo() {
try {
const systemInfo = await window.electronAPI.getSystemInfo()
const version = await window.electronAPI.getAppVersion()
document.getElementById('platform').textContent = systemInfo.platform
document.getElementById('arch').textContent = systemInfo.arch
document.getElementById('electron-version').textContent = systemInfo.electron
document.getElementById('chrome-version').textContent = systemInfo.chrome
document.getElementById('node-version').textContent = systemInfo.node
document.getElementById('app-version').textContent = version
} catch (error) {
console.error('Failed to load system info:', error)
}
}
// 计数器功能
let count = 0
document.getElementById('decrease').addEventListener('click', () => {
count--
document.getElementById('count').textContent = count
})
document.getElementById('increase').addEventListener('click', () => {
count++
document.getElementById('count').textContent = count
})
// 消息功能
document.getElementById('send-message').addEventListener('click', () => {
const input = document.getElementById('message-input')
const message = input.value.trim()
if (message) {
addMessage(message)
input.value = ''
}
})
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('send-message').click()
}
})
function addMessage(text) {
const messagesDiv = document.getElementById('messages')
const messageElement = document.createElement('div')
messageElement.className = 'message-item'
messageElement.textContent = `${new Date().toLocaleTimeString()}: ${text}`
messagesDiv.appendChild(messageElement)
messagesDiv.scrollTop = messagesDiv.scrollHeight
}
// 打开外部链接(需要在主进程实现)
function openExternal(url) {
// 这里需要通过 IPC 调用主进程的 shell.openExternal
alert(`将打开: ${url}`)
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
loadSystemInfo()
addMessage('欢迎使用 Electron 应用!')
})运行应用
bash
# 开发模式运行
npm start
# 或
electron .调试技巧
1. 主进程调试
bash
# 启用调试模式
electron --inspect=5858 .
# 或在 package.json 中
"scripts": {
"dev": "electron . --inspect=5858"
}然后在 Chrome 中访问 chrome://inspect
2. 渲染进程调试
javascript
// main.js 中打开开发者工具
mainWindow.webContents.openDevTools()
// 或按 F12 / Cmd+Option+I3. 日志输出
javascript
// 主进程
console.log('Main process:', data)
// 渲染进程
console.log('Renderer process:', data)常见问题
问题 1:窗口一闪而过
javascript
// 原因:应用在窗口创建前就退出了
// 解决方案:正确处理应用生命周期
app.whenReady().then(() => {
createWindow()
})问题 2:require is not defined
javascript
// 原因:渲染进程中直接使用 require
// 解决方案:使用 preload 脚本和 contextBridge
// ❌ 错误做法(渲染进程)
const fs = require('fs')
// ✅ 正确做法(preload.js)
contextBridge.exposeInMainWorld('fs', {
readFile: (path) => fs.readFile(path, 'utf8')
})问题 3:跨域问题
javascript
// 配置 CSP
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">总结
🎉 恭喜!你已经创建了第一个 Electron 应用!
✅ 学到的知识点
项目结构
- package.json 配置
- main.js (主进程)
- preload.js (预加载脚本)
- index.html (渲染进程)
核心概念
- 主进程和渲染进程
- IPC 通信
- contextBridge 安全通信
基本功能
- 窗口创建
- 系统信息获取
- 界面交互
🚀 下一步
下一篇文章,我们将深入学习主进程和渲染进程的区别和职责。
相关文章推荐:
有问题欢迎留言讨论!