Electron
概述
Electron 是一个使用 Web 技术(HTML、CSS、JavaScript)构建跨平台桌面应用的开源框架。由 GitHub 开发维护,VS Code、Slack、Discord、Figma、Notion 等知名应用均基于 Electron。
- 官网: electronjs.org
- 核心思路: Chromium + Node.js + 原生 API
架构:双进程模型
Electron 的核心架构分为两个进程:
┌─────────────────────────────────────┐
│ Main Process │
│ (主进程 - Node.js 环境) │
│ 管理窗口、菜单、托盘、原生 API │
│ ipcMain.handle() 处理渲染进程请求 │
└────────────┬────────────────────────┘
│ IPC (进程间通信)
┌────────┴────────┬───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Renderer │ │ Renderer │ │ Renderer │
│ 进程1 │ │ 进程2 │ │ 进程3 │
│ Chromium │ │ Chromium │ │ Chromium │
└──────────┘ └──────────┘ └──────────┘┌─────────────────────────────────────┐
│ Main Process │
│ (主进程 - Node.js 环境) │
│ 管理窗口、菜单、托盘、原生 API │
│ ipcMain.handle() 处理渲染进程请求 │
└────────────┬────────────────────────┘
│ IPC (进程间通信)
┌────────┴────────┬───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Renderer │ │ Renderer │ │ Renderer │
│ 进程1 │ │ 进程2 │ │ 进程3 │
│ Chromium │ │ Chromium │ │ Chromium │
└──────────┘ └──────────┘ └──────────┘主进程(Main Process)
- 应用的入口点,通常是一个
main.js文件 - 运行在 Node.js 环境中,有完整的系统权限
- 负责:创建窗口(BrowserWindow)、管理应用生命周期、调用原生 API
- 整个应用只有一个主进程
渲染进程(Renderer Process)
- 每个 BrowserWindow 对应一个独立的渲染进程
- 运行在 Chromium 沙箱中,默认不能直接访问 Node.js API
- 负责:UI 渲染、用户交互
Preload 脚本
- 介于主进程和渲染进程之间的"桥梁"
- 在渲染进程创建前执行,可以有限地访问 Node.js API
- 通过
contextBridge安全地暴露 API 给渲染进程
快速开始
安装
bash
npm init -y
npm install electron --save-devnpm init -y
npm install electron --save-dev项目结构
my-app/
├── main.js # 主进程入口
├── preload.js # 预加载脚本
├── index.html # 渲染页面
└── package.jsonmy-app/
├── main.js # 主进程入口
├── preload.js # 预加载脚本
├── index.html # 渲染页面
└── package.json主进程(main.js)
js
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必须开启
nodeIntegration: false, // 必须关闭
sandbox: true, // 推荐开启
},
})
win.loadFile('index.html')
// win.loadURL('http://localhost:5173') // Vite 开发模式
}
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()
})const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必须开启
nodeIntegration: false, // 必须关闭
sandbox: true, // 推荐开启
},
})
win.loadFile('index.html')
// win.loadURL('http://localhost:5173') // Vite 开发模式
}
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()
})Preload 脚本(preload.js)
js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 双向通信:渲染进程 → 主进程(Promise)
getVersion: () => ipcRenderer.invoke('get-version'),
// 单向通信:渲染进程 → 主进程
saveFile: (data) => ipcRenderer.send('save-file', data),
// 主进程 → 渲染进程(监听)
onUpdate: (callback) => {
ipcRenderer.on('update-available', (_event, data) => callback(data))
},
// 移除监听
removeListener: (channel) => {
ipcRenderer.removeAllListeners(channel)
},
})const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 双向通信:渲染进程 → 主进程(Promise)
getVersion: () => ipcRenderer.invoke('get-version'),
// 单向通信:渲染进程 → 主进程
saveFile: (data) => ipcRenderer.send('save-file', data),
// 主进程 → 渲染进程(监听)
onUpdate: (callback) => {
ipcRenderer.on('update-available', (_event, data) => callback(data))
},
// 移除监听
removeListener: (channel) => {
ipcRenderer.removeAllListeners(channel)
},
})主进程处理 IPC
js
const { ipcMain } = require('electron')
ipcMain.handle('get-version', () => {
return app.getVersion()
})
ipcMain.on('save-file', (event, data) => {
// 保存文件逻辑
})const { ipcMain } = require('electron')
ipcMain.handle('get-version', () => {
return app.getVersion()
})
ipcMain.on('save-file', (event, data) => {
// 保存文件逻辑
})渲染进程中使用
html
<!DOCTYPE html>
<html>
<body>
<button id="btn">获取版本号</button>
<script>
document.getElementById('btn').addEventListener('click', async () => {
const version = await window.electronAPI.getVersion()
alert('版本: ' + version)
})
</script>
</body>
</html><!DOCTYPE html>
<html>
<body>
<button id="btn">获取版本号</button>
<script>
document.getElementById('btn').addEventListener('click', async () => {
const version = await window.electronAPI.getVersion()
alert('版本: ' + version)
})
</script>
</body>
</html>BrowserWindow 配置
常用选项
js
const win = new BrowserWindow({
width: 1200, // 窗口宽度
height: 800, // 窗口高度
minWidth: 800, // 最小宽度
minHeight: 600, // 最小高度
x: undefined, // 初始位置 X
y: undefined, // 初始位置 Y
show: false, // 创建后是否显示(配合 ready-to-show)
center: true, // 是否居中
resizable: true, // 是否可调整大小
maximizable: true, // 是否可最大化
minimizable: true, // 是否可最小化
closable: true, // 是否可关闭
alwaysOnTop: false, // 是否置顶
fullscreen: false, // 是否全屏
titleBarStyle: 'default', // macOS 标题栏样式
frame: true, // 是否有边框
transparent: false, // 是否透明
icon: path.join(__dirname, 'icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true, // 是否启用同源策略
},
})const win = new BrowserWindow({
width: 1200, // 窗口宽度
height: 800, // 窗口高度
minWidth: 800, // 最小宽度
minHeight: 600, // 最小高度
x: undefined, // 初始位置 X
y: undefined, // 初始位置 Y
show: false, // 创建后是否显示(配合 ready-to-show)
center: true, // 是否居中
resizable: true, // 是否可调整大小
maximizable: true, // 是否可最大化
minimizable: true, // 是否可最小化
closable: true, // 是否可关闭
alwaysOnTop: false, // 是否置顶
fullscreen: false, // 是否全屏
titleBarStyle: 'default', // macOS 标题栏样式
frame: true, // 是否有边框
transparent: false, // 是否透明
icon: path.join(__dirname, 'icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true, // 是否启用同源策略
},
})生命周期
js
win.on('ready-to-show', () => { win.show() }) // 窗口准备就绪
win.on('closed', () => { win = null }) // 窗口关闭
win.on('focus', () => {}) // 获得焦点
win.on('blur', () => {}) // 失去焦点
win.on('maximize', () => {}) // 最大化
win.on('minimize', () => {}) // 最小化
win.on('restore', () => {}) // 还原win.on('ready-to-show', () => { win.show() }) // 窗口准备就绪
win.on('closed', () => { win = null }) // 窗口关闭
win.on('focus', () => {}) // 获得焦点
win.on('blur', () => {}) // 失去焦点
win.on('maximize', () => {}) // 最大化
win.on('minimize', () => {}) // 最小化
win.on('restore', () => {}) // 还原常用方法
js
win.show() // 显示
win.hide() // 隐藏
win.close() // 关闭
win.minimize() // 最小化
win.maximize() // 最大化
win.unmaximize() // 还原
win.focus() // 聚焦
win.reload() // 刷新
win.loadFile('index.html') // 加载本地文件
win.loadURL('https://...') // 加载远程 URL
win.webContents.openDevTools() // 打开开发者工具win.show() // 显示
win.hide() // 隐藏
win.close() // 关闭
win.minimize() // 最小化
win.maximize() // 最大化
win.unmaximize() // 还原
win.focus() // 聚焦
win.reload() // 刷新
win.loadFile('index.html') // 加载本地文件
win.loadURL('https://...') // 加载远程 URL
win.webContents.openDevTools() // 打开开发者工具IPC 通信详解
模式对比
| 模式 | 方向 | API | 返回 |
|---|---|---|---|
| invoke/handle | 渲染→主 | invoke / handle | Promise(推荐) |
| send/on | 渲染→主 | send / on | 无返回值 |
| send/on | 主→渲染 | webContents.send / on | 单向推送 |
invoke / handle(双向,推荐)
js
// 主进程
ipcMain.handle('read-file', async (_event, filePath) => {
const fs = require('fs')
return fs.readFileSync(filePath, 'utf-8')
})
// 渲染进程(通过 preload 暴露)
const content = await window.electronAPI.readFile('/path/to/file')// 主进程
ipcMain.handle('read-file', async (_event, filePath) => {
const fs = require('fs')
return fs.readFileSync(filePath, 'utf-8')
})
// 渲染进程(通过 preload 暴露)
const content = await window.electronAPI.readFile('/path/to/file')send / on(单向推送,主进程 → 渲染进程)
js
// 主进程
win.webContents.send('download-progress', { percent: 50 })
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onDownloadProgress: (callback) => {
ipcRenderer.on('download-progress', (_event, data) => callback(data))
},
})
// 渲染进程
window.electronAPI.onDownloadProgress((data) => {
console.log(data.percent) // 50
})// 主进程
win.webContents.send('download-progress', { percent: 50 })
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onDownloadProgress: (callback) => {
ipcRenderer.on('download-progress', (_event, data) => callback(data))
},
})
// 渲染进程
window.electronAPI.onDownloadProgress((data) => {
console.log(data.percent) // 50
})安全实践
js
// preload.js — 始终白名单通道
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain'] // 白名单
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
invoke: (channel, data) => {
const validChannels = ['get-version', 'read-file']
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, data)
}
},
})// preload.js — 始终白名单通道
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain'] // 白名单
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
invoke: (channel, data) => {
const validChannels = ['get-version', 'read-file']
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, data)
}
},
})原生 API
菜单(Menu)
js
const { Menu, shell } = require('electron')
const template = [
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: () => {} },
{ label: '打开', accelerator: 'CmdOrCtrl+O', click: () => {} },
{ type: 'separator' },
{ role: 'quit', label: '退出' },
],
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
],
},
{
label: '帮助',
submenu: [
{
label: '打开开发者工具',
accelerator: 'F12',
click: (_, focusedWindow) => focusedWindow?.toggleDevTools(),
},
],
},
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)const { Menu, shell } = require('electron')
const template = [
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: () => {} },
{ label: '打开', accelerator: 'CmdOrCtrl+O', click: () => {} },
{ type: 'separator' },
{ role: 'quit', label: '退出' },
],
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
],
},
{
label: '帮助',
submenu: [
{
label: '打开开发者工具',
accelerator: 'F12',
click: (_, focusedWindow) => focusedWindow?.toggleDevTools(),
},
],
},
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)托盘(Tray)
js
const { Tray, Menu, nativeImage } = require('electron')
const tray = new Tray(nativeImage.createFromPath('icon.png'))
tray.setToolTip('我的应用')
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '隐藏窗口', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() },
])
tray.setContextMenu(contextMenu)
tray.on('click', () => { mainWindow.show() })const { Tray, Menu, nativeImage } = require('electron')
const tray = new Tray(nativeImage.createFromPath('icon.png'))
tray.setToolTip('我的应用')
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '隐藏窗口', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() },
])
tray.setContextMenu(contextMenu)
tray.on('click', () => { mainWindow.show() })对话框(Dialog)
js
const { dialog } = require('electron')
// 打开文件
const result = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{ name: '图片', extensions: ['jpg', 'png', 'gif'] }],
})
// 保存文件
const result = await dialog.showSaveDialog({
defaultPath: '下载文件.txt',
filters: [{ name: '文本', extensions: ['txt'] }],
})
// 消息提示
await dialog.showMessageBox({
type: 'info',
title: '提示',
message: '操作成功',
buttons: ['确定'],
})const { dialog } = require('electron')
// 打开文件
const result = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{ name: '图片', extensions: ['jpg', 'png', 'gif'] }],
})
// 保存文件
const result = await dialog.showSaveDialog({
defaultPath: '下载文件.txt',
filters: [{ name: '文本', extensions: ['txt'] }],
})
// 消息提示
await dialog.showMessageBox({
type: 'info',
title: '提示',
message: '操作成功',
buttons: ['确定'],
})系统通知
js
const { Notification } = require('electron')
new Notification({
title: '消息',
body: '你有一条新消息',
icon: 'icon.png',
}).show()const { Notification } = require('electron')
new Notification({
title: '消息',
body: '你有一条新消息',
icon: 'icon.png',
}).show()打包与发布
electron-builder(推荐)
bash
npm install electron-builder --save-devnpm install electron-builder --save-devjson
// package.json
{
"build": {
"appId": "com.example.myapp",
"productName": "MyApp",
"directories": {
"output": "release"
},
"files": [
"main.js",
"preload.js",
"renderer/**/*"
],
"win": {
"target": ["nsis", "portable"],
"icon": "build/icon.ico"
},
"mac": {
"target": ["dmg"],
"icon": "build/icon.icns",
"notarize": true
},
"linux": {
"target": ["AppImage", "deb"],
"icon": "build/icon.png"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}// package.json
{
"build": {
"appId": "com.example.myapp",
"productName": "MyApp",
"directories": {
"output": "release"
},
"files": [
"main.js",
"preload.js",
"renderer/**/*"
],
"win": {
"target": ["nsis", "portable"],
"icon": "build/icon.ico"
},
"mac": {
"target": ["dmg"],
"icon": "build/icon.icns",
"notarize": true
},
"linux": {
"target": ["AppImage", "deb"],
"icon": "build/icon.png"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}打包命令:
bash
npx electron-builder --win # Windows
npx electron-builder --mac # macOS
npx electron-builder --linux # Linux
npx electron-builder --win --mac # 跨平台npx electron-builder --win # Windows
npx electron-builder --mac # macOS
npx electron-builder --linux # Linux
npx electron-builder --win --mac # 跨平台electron-forge
bash
npm install @electron-forge/cli --save-dev
npx electron-forge import
npm run make # 打包
npm run publish # 发布npm install @electron-forge/cli --save-dev
npx electron-forge import
npm run make # 打包
npm run publish # 发布自动更新
js
const { autoUpdater } = require('electron-updater')
autoUpdater.on('checking-for-update', () => {
console.log('检查更新...')
})
autoUpdater.on('update-available', (info) => {
console.log('发现新版本:', info.version)
})
autoUpdater.on('download-progress', (progress) => {
console.log(`下载进度: ${progress.percent}%`)
})
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
// 检查更新
autoUpdater.checkForUpdates()const { autoUpdater } = require('electron-updater')
autoUpdater.on('checking-for-update', () => {
console.log('检查更新...')
})
autoUpdater.on('update-available', (info) => {
console.log('发现新版本:', info.version)
})
autoUpdater.on('download-progress', (progress) => {
console.log(`下载进度: ${progress.percent}%`)
})
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
// 检查更新
autoUpdater.checkForUpdates()安全最佳实践
| 设置 | 推荐值 | 原因 |
|---|---|---|
contextIsolation | true | 隔离 preload 与渲染进程上下文 |
nodeIntegration | false | 禁止渲染进程直接访问 Node.js |
sandbox | true | 沙箱化渲染进程 |
webSecurity | true | 启用同源策略 |
Content Security Policy
html
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/><meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/>不要暴露完整 ipcRenderer
js
// ❌ 错误:暴露完整 ipcRenderer
contextBridge.exposeInMainWorld('electronAPI', ipcRenderer)
// ✅ 正确:只暴露特定方法
contextBridge.exposeInMainWorld('electronAPI', {
ping: () => ipcRenderer.invoke('ping'),
})// ❌ 错误:暴露完整 ipcRenderer
contextBridge.exposeInMainWorld('electronAPI', ipcRenderer)
// ✅ 正确:只暴露特定方法
contextBridge.exposeInMainWorld('electronAPI', {
ping: () => ipcRenderer.invoke('ping'),
})开发工具推荐
| 工具 | 用途 |
|---|---|
| electron-vite | Vite 驱动的 Electron 开发模板 |
| electron-builder | 打包、签名、安装包生成 |
| electron-updater | 自动更新 |
| electron-devtools-installer | 安装 React/Vue DevTools |
| electron-store | 持久化存储(类似 localStorage) |
electron-vite 项目结构
my-app/
├── src/
│ ├── main/ # 主进程代码
│ ├── preload/ # preload 脚本
│ └── renderer/ # 渲染进程(React/Vue)
├── electron.vite.config.ts
└── package.jsonmy-app/
├── src/
│ ├── main/ # 主进程代码
│ ├── preload/ # preload 脚本
│ └── renderer/ # 渲染进程(React/Vue)
├── electron.vite.config.ts
└── package.jsonts
// electron.vite.config.ts
import { defineConfig } from 'electron-vite'
export default defineConfig({
main: { build: { rollupOptions: { input: 'src/main/index.ts' } } },
preload: { build: { rollupOptions: { input: 'src/preload/index.ts' } } },
renderer: {},
})// electron.vite.config.ts
import { defineConfig } from 'electron-vite'
export default defineConfig({
main: { build: { rollupOptions: { input: 'src/main/index.ts' } } },
preload: { build: { rollupOptions: { input: 'src/preload/index.ts' } } },
renderer: {},
})常见注意事项
- Chromium 内存占用 — 每个渲染进程约 100MB 基线
- 打包体积大 — 基础 runtime 约 50MB+(含 Chromium + Node.js)
- macOS 需要公证 — 自 Catalina 起必须 notarize
- 开发/生产判断 —
p或rocess.env.NODE_ENV app.isPackaged - GPU 加速 — 默认开启,部分系统需要
--disable-gpu标志 - 启动优化 — 使用
show: false+ready-to-show事件避免白屏
js
const win = new BrowserWindow({ show: false })
win.once('ready-to-show', () => win.show())const win = new BrowserWindow({ show: false })
win.once('ready-to-show', () => win.show())
Cao Note