产品经理说"能不能做成桌面应用"。我第一反应是:我又不会 C++/C#。后来发现 Electron 就是给前端准备的------用 HTML/CSS/JS 写桌面应用,VS Code、Discord、Slack 都是这么做的。
基本概念
Electron = Chromium(渲染页面)+ Node.js(系统能力)
scss
┌─────────────────────────┐
│ Electron App │
├──────────┬──────────────┤
│ 主进程 │ 渲染进程 │
│ (Node.js)│ (Chromium) │
│ │ │
│ - 系统API │ - 页面渲染 │
│ - 文件操作 │ - DOM/CSS │
│ - 菜单 │ - 用户交互 │
│ - 托盘 │ │
└──────────┴──────────────┘
- 主进程(Main):Node.js 环境,可以操作文件系统、创建窗口、访问系统 API
- 渲染进程(Renderer):就是浏览器环境,跑你的前端页面
快速开始
bash
npm create electron-app@latest my-app
cd my-app
npm start
项目结构:
perl
my-app/
├── src/
│ ├── main.js # 主进程入口
│ ├── preload.js # 预加载脚本(主进程和渲染进程之间的桥梁)
│ └── renderer/ # 前端页面(可以是 Vue/React 项目)
├── package.json
└── electron-builder.yml
主进程:创建窗口
js
// src/main.js
const { app, BrowserWindow, Menu, Tray } = require('electron')
const path = require('path')
let mainWindow = null
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 安全:隔离上下文
nodeIntegration: false, // 安全:不直接暴露 Node
}
})
// 开发环境加载 localhost,生产环境加载打包后的文件
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit() // macOS 关闭窗口不退出应用
}
})
主进程和渲染进程通信
js
// preload.js --- 暴露安全的 API 给渲染进程
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 读取文件
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
// 保存文件
saveFile: (filePath, content) => ipcRenderer.invoke('save-file', filePath, content),
// 选择文件夹
selectFolder: () => ipcRenderer.invoke('select-folder'),
// 监听主进程消息
onNotification: (callback) => ipcRenderer.on('notification', (_, data) => callback(data)),
})
// main.js --- 处理渲染进程的请求
const { ipcMain, dialog } = require('electron')
const fs = require('fs/promises')
ipcMain.handle('read-file', async (_, filePath) => {
return await fs.readFile(filePath, 'utf-8')
})
ipcMain.handle('save-file', async (_, filePath, content) => {
await fs.writeFile(filePath, content, 'utf-8')
})
ipcMain.handle('select-folder', async () => {
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] })
return result.filePaths[0]
})
js
// 渲染进程中使用
async function openFile() {
const content = await window.electronAPI.readFile('/path/to/file.txt')
console.log(content)
}
async function chooseFolder() {
const folder = await window.electronAPI.selectFolder()
console.log('选择了:', folder)
}
系统托盘
js
const { app, Tray, Menu } = require('electron')
let tray = null
app.whenReady().then(() => {
tray = new Tray(path.join(__dirname, 'icon.png'))
const contextMenu = Menu.buildFromTemplate([
{ label: '显示主窗口', click: () => mainWindow.show() },
{ label: '退出', click: () => app.quit() },
])
tray.setToolTip('我的应用')
tray.setContextMenu(contextMenu)
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
})
打包发布
yaml
# electron-builder.yml
appId: com.example.myapp
productName: My App
directories:
output: release
files:
- dist/**
- src/main.js
- src/preload.js
win:
target: nsis
icon: build/icon.ico
mac:
target: dmg
icon: build/icon.icns
linux:
target: AppImage
bash
# 打包
npm run build # 先构建前端
npx electron-builder # 打包桌面应用
输出:Windows 的 .exe 安装包、macOS 的 .dmg、Linux 的 .AppImage。
注意事项
- 包体积大:Electron 自带 Chromium,最小也有 100MB+。如果在意体积考虑 Tauri(Rust + 系统 WebView)
- 内存占用高:每个窗口就是一个 Chromium 实例
- 安全第一 :
contextIsolation: true+nodeIntegration: false,不要直接暴露 Node API 给渲染进程 - 自动更新 :用
electron-updater实现应用内自动更新
总结
Electron 让前端能写桌面应用,核心就是:主进程管系统能力(文件、菜单、托盘),渲染进程跑前端页面,两者通过 IPC 通信。VS Code、Notion、Figma 桌面版都是 Electron 做的。包体积大是最大缺点,不在意这个的话,前端写桌面应用首选 Electron。