Skip to content

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-dev
npm init -y
npm install electron --save-dev

项目结构 ​

my-app/
├── main.js          # 主进程入口
├── preload.js       # 预加载脚本
├── index.html       # 渲染页面
└── package.json
my-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 / handlePromise(推荐)
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-dev
npm install electron-builder --save-dev
json
// 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()

安全最佳实践 ​

设置推荐值原因
contextIsolationtrue隔离 preload 与渲染进程上下文
nodeIntegrationfalse禁止渲染进程直接访问 Node.js
sandboxtrue沙箱化渲染进程
webSecuritytrue启用同源策略

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-viteVite 驱动的 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.json
my-app/
├── src/
│   ├── main/          # 主进程代码
│   ├── preload/       # preload 脚本
│   └── renderer/      # 渲染进程(React/Vue)
├── electron.vite.config.ts
└── package.json
ts
// 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: {},
})

常见注意事项 ​

  1. Chromium 内存占用 — 每个渲染进程约 100MB 基线
  2. 打包体积大 — 基础 runtime 约 50MB+(含 Chromium + Node.js)
  3. macOS 需要公证 — 自 Catalina 起必须 notarize
  4. 开发/生产判断 — process.env.NODE_ENV 或 app.isPackaged
  5. GPU 加速 — 默认开启,部分系统需要 --disable-gpu 标志
  6. 启动优化 — 使用 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())

参考资料 ​