Skip to content

Electron 桌面应用开发完全指南

更新时间:2025-02

目录导航

Electron 简介

什么是 Electron

Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的框架。它将 Chromium 和 Node.js 嵌入到一个运行时中,允许开发者使用 Web 技术开发桌面应用。

核心特点

  • 跨平台:一套代码,支持 Windows、macOS、Linux
  • Web 技术栈:使用熟悉的前端技术
  • 原生能力:访问文件系统、系统托盘、通知等
  • 丰富生态:npm 生态系统支持
  • 活跃社区:大量开源项目和插件

知名应用

  • VS Code(微软)
  • Slack(团队协作)
  • Discord(游戏社交)
  • Figma(设计工具)
  • Postman(API 测试)

Electron 架构

Electron 采用多进程架构,类似于 Chromium 浏览器:

┌─────────────────────────────────────────┐
│         主进程 (Main Process)            │
│  - Node.js 运行时                        │
│  - 管理应用生命周期                       │
│  - 创建和管理窗口                         │
│  - 访问原生 API                          │
└─────────────────────────────────────────┘
              ↓ 创建
┌─────────────────────────────────────────┐
│      渲染进程 (Renderer Process)         │
│  - Chromium 运行时                       │
│  - 渲染 Web 页面                         │
│  - 执行前端代码                          │
│  - 每个窗口一个进程                       │
└─────────────────────────────────────────┘
              ↕ IPC 通信
┌─────────────────────────────────────────┐
│      预加载脚本 (Preload Script)         │
│  - 桥接主进程和渲染进程                   │
│  - 暴露安全的 API                        │
│  - 在渲染进程加载前执行                   │
└─────────────────────────────────────────┘

主进程 vs 渲染进程

特性主进程渲染进程
数量1 个多个(每个窗口一个)
运行时Node.jsChromium
入口文件main.jsindex.html
能力完整的 Node.js API受限的 Node.js API
原生 API可直接访问需通过 IPC 访问

快速开始

环境搭建

前置要求

  • Node.js 16+
  • npm 或 yarn 或 pnpm

安装 Electron

bash
# 创建项目目录
mkdir my-electron-app
cd my-electron-app

# 初始化项目
npm init -y

# 安装 Electron
npm install electron --save-dev

创建第一个应用

1. 项目结构

my-electron-app/
├── main.js           # 主进程入口
├── preload.js        # 预加载脚本
├── index.html        # 渲染进程页面
├── renderer.js       # 渲染进程脚本
└── package.json      # 项目配置

2. 配置 package.json

json
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^28.0.0"
  }
}

3. 主进程代码(main.js)

javascript
const { app, BrowserWindow } = require('electron')
const path = require('path')

// 创建窗口函数
function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,  // 启用上下文隔离(安全)
      nodeIntegration: false   // 禁用 Node.js 集成(安全)
    }
  })

  // 加载页面
  win.loadFile('index.html')

  // 打开开发者工具(开发环境)
  if (process.env.NODE_ENV === 'development') {
    win.webContents.openDevTools()
  }
}

// 应用准备就绪时创建窗口
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()
  }
})

4. 预加载脚本(preload.js)

javascript
const { contextBridge, ipcRenderer } = require('electron')

// 暴露安全的 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // 发送消息到主进程
  sendMessage: (message) => ipcRenderer.send('message', message),
  
  // 接收主进程消息
  onReply: (callback) => ipcRenderer.on('reply', (event, data) => callback(data)),
  
  // 获取平台信息
  platform: process.platform,
  
  // 获取版本信息
  versions: {
    node: process.versions.node,
    chrome: process.versions.chrome,
    electron: process.versions.electron
  }
})

5. 渲染进程页面(index.html)

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>My Electron App</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 20px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      min-height: 100vh;
      margin: 0;
    }
    .container {
      max-width: 600px;
      margin: 0 auto;
      background: rgba(255, 255, 255, 0.1);
      padding: 30px;
      border-radius: 10px;
      backdrop-filter: blur(10px);
    }
    h1 { margin-top: 0; }
    .info { margin: 20px 0; }
    button {
      background: white;
      color: #667eea;
      border: none;
      padding: 10px 20px;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
    }
    button:hover { opacity: 0.9; }
    #response { margin-top: 20px; }
  </style>
</head>
<body>
  <div class="container">
    <h1>🚀 欢迎使用 Electron</h1>
    
    <div class="info">
      <p><strong>平台:</strong><span id="platform"></span></p>
      <p><strong>Node.js:</strong><span id="node-version"></span></p>
      <p><strong>Chrome:</strong><span id="chrome-version"></span></p>
      <p><strong>Electron:</strong><span id="electron-version"></span></p>
    </div>

    <button id="send-btn">发送消息到主进程</button>
    <div id="response"></div>
  </div>

  <script src="renderer.js"></script>
</body>
</html>

6. 渲染进程脚本(renderer.js)

javascript
// 显示版本信息
document.getElementById('platform').textContent = window.electronAPI.platform
document.getElementById('node-version').textContent = window.electronAPI.versions.node
document.getElementById('chrome-version').textContent = window.electronAPI.versions.chrome
document.getElementById('electron-version').textContent = window.electronAPI.versions.electron

// 发送消息
document.getElementById('send-btn').addEventListener('click', () => {
  window.electronAPI.sendMessage('Hello from Renderer!')
})

// 接收回复
window.electronAPI.onReply((data) => {
  document.getElementById('response').innerHTML = `
    <p><strong>主进程回复:</strong>${data}</p>
  `
})

7. 运行应用

bash
npm start

进程通信(IPC)

IPC(Inter-Process Communication)是 Electron 中主进程和渲染进程之间通信的核心机制。

IPC 通信方式

1. 渲染进程 → 主进程(单向)

javascript
// 渲染进程(通过 preload.js 暴露的 API)
window.electronAPI.sendMessage('ping')

// 主进程(main.js)
const { ipcMain } = require('electron')

ipcMain.on('message', (event, data) => {
  console.log('收到消息:', data)
})

2. 渲染进程 → 主进程 → 渲染进程(双向)

javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  invoke: (channel, data) => ipcRenderer.invoke(channel, data)
})

// 渲染进程
const result = await window.electronAPI.invoke('get-data', { id: 1 })
console.log(result)

// 主进程
ipcMain.handle('get-data', async (event, args) => {
  // 处理请求
  return { success: true, data: args }
})

3. 主进程 → 渲染进程

javascript
// 主进程
const { BrowserWindow } = require('electron')

const win = BrowserWindow.getFocusedWindow()
win.webContents.send('update-data', { count: 100 })

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  onUpdate: (callback) => ipcRenderer.on('update-data', (event, data) => callback(data))
})

// 渲染进程
window.electronAPI.onUpdate((data) => {
  console.log('收到更新:', data)
})

IPC 实战案例

案例 1:文件选择对话框

javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  selectFile: () => ipcRenderer.invoke('select-file')
})

// 主进程
const { dialog } = require('electron')

ipcMain.handle('select-file', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  })
  
  if (!result.canceled) {
    return result.filePaths[0]
  }
  return null
})

// 渲染进程
document.getElementById('select-btn').addEventListener('click', async () => {
  const filePath = await window.electronAPI.selectFile()
  if (filePath) {
    console.log('选择的文件:', filePath)
  }
})

案例 2:读取文件内容

javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
})

// 主进程
const fs = require('fs').promises

ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const content = await fs.readFile(filePath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 渲染进程
const result = await window.electronAPI.readFile('/path/to/file.txt')
if (result.success) {
  console.log('文件内容:', result.content)
}

案例 3:进度通知

javascript
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  startTask: () => ipcRenderer.send('start-task'),
  onProgress: (callback) => ipcRenderer.on('task-progress', (event, data) => callback(data))
})

// 主进程
ipcMain.on('start-task', (event) => {
  let progress = 0
  const interval = setInterval(() => {
    progress += 10
    event.sender.send('task-progress', { progress })
    
    if (progress >= 100) {
      clearInterval(interval)
    }
  }, 500)
})

// 渲染进程
window.electronAPI.startTask()
window.electronAPI.onProgress((data) => {
  console.log('进度:', data.progress + '%')
  document.getElementById('progress').style.width = data.progress + '%'
})

MessagePort 通信

MessagePort 提供了更灵活的通信方式,适用于复杂场景:

javascript
// 主进程
const { MessageChannelMain } = require('electron')

const { port1, port2 } = new MessageChannelMain()

// 发送 port2 到渲染进程
win.webContents.postMessage('port', null, [port2])

// 监听 port1 消息
port1.on('message', (event) => {
  console.log('收到消息:', event.data)
})

port1.start()

// 渲染进程
window.addEventListener('message', (event) => {
  const [port] = event.ports
  
  // 发送消息
  port.postMessage('Hello from renderer')
  
  // 接收消息
  port.onmessage = (event) => {
    console.log('收到回复:', event.data)
  }
})

窗口管理

BrowserWindow 配置

javascript
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({
  // 窗口尺寸
  width: 1200,
  height: 800,
  minWidth: 800,
  minHeight: 600,
  
  // 窗口位置
  x: 100,
  y: 100,
  center: true,  // 居中显示
  
  // 窗口样式
  title: 'My App',
  icon: path.join(__dirname, 'icon.png'),
  backgroundColor: '#ffffff',
  
  // 窗口行为
  resizable: true,
  movable: true,
  minimizable: true,
  maximizable: true,
  closable: true,
  
  // 窗口类型
  frame: true,        // 显示边框
  transparent: false, // 透明窗口
  alwaysOnTop: false, // 置顶
  
  // 显示方式
  show: false,        // 创建时不显示(配合 ready-to-show 事件)
  
  // Web 偏好设置
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true
  }
})

// 页面加载完成后显示(避免白屏)
win.once('ready-to-show', () => {
  win.show()
})

多窗口管理

创建子窗口

javascript
let mainWindow = null
let childWindow = null

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800
  })
  
  mainWindow.loadFile('index.html')
  
  mainWindow.on('closed', () => {
    mainWindow = null
    // 关闭所有子窗口
    if (childWindow) {
      childWindow.close()
    }
  })
}

function createChildWindow() {
  childWindow = new Brows
ow.webContents.send('message-from-child', { data: 'Hi' })
}

// 获取所有窗口
const allWindows = BrowserWindow.getAllWindows()
allWindows.forEach(win => {
  win.webContents.send('broadcast', { message: 'Hello all' })
})

无边框窗口

javascript
const win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,           // 无边框
  transparent: true,      // 透明背景
  titleBarStyle: 'hidden' // macOS 隐藏标题栏
})

// 自定义拖拽区域(CSS)
css
/* 在 HTML 中定义可拖拽区域 */
.titlebar {
  -webkit-app-region: drag;  /* 可拖拽 */
  height: 32px;
  background: #333;
}

.titlebar button {
  -webkit-app-region: no-drag;  /* 按钮不可拖拽 */
}
html
<div class="titlebar">
  <span>My App</span>
  <button id="minimize">−</button>
  <button id="maximize">□</button>
  <button id="close">×</button>
</div>
javascript
// 渲染进程控制窗口
// preload.js
contextBridge.exposeInMainWorld('windowAPI', {
  minimize: () => ipcRenderer.send('window-minimize'),
  maximize: () => ipcRenderer.send('window-maximize'),
  close: () => ipcRenderer.send('window-close')
})

// 主进程
ipcMain.on('window-minimize', (event) => {
  BrowserWindow.fromWebContents(event.sender).minimize()
})

ipcMain.on('window-maximize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (win.isMaximized()) {
    win.unmaximize()
  } else {
    win.maximize()
  }
})

ipcMain.on('window-close', (event) => {
  BrowserWindow.fromWebContents(event.sender).close()
})

窗口状态保存

javascript
const Store = require('electron-store')
const store = new Store()

function createWindow() {
  // 恢复窗口状态
  const windowState = store.get('windowState', {
    width: 1200,
    height: 800,
    x: undefined,
    y: undefined
  })
  
  const win = new BrowserWindow({
    width: windowState.width,
    height: windowState.height,
    x: windowState.x,
    y: windowState.y
  })
  
  // 保存窗口状态
  const saveWindowState = () => {
    const bounds = win.getBounds()
    store.set('windowState', bounds)
  }
  
  win.on('resize', saveWindowState)
  win.on('move', saveWindowState)
  
  return win
}

原生能力

文件系统操作

javascript
// 主进程
const { dialog } = require('electron')
const fs = require('fs').promises
const path = require('path')

// 选择文件
ipcMain.handle('select-file', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile', 'multiSelections'],
    filters: [
      { name: 'Text Files', extensions: ['txt', 'md'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  })
  return result.filePaths
})

// 选择文件夹
ipcMain.handle('select-folder', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openDirectory']
  })
  return result.filePaths[0]
})

// 保存文件
ipcMain.handle('save-file', async (event, content) => {
  const result = await dialog.showSaveDialog({
    defaultPath: 'untitled.txt',
    filters: [
      { name: 'Text Files', extensions: ['txt'] }
    ]
  })
  
  if (!result.canceled) {
    await fs.writeFile(result.filePath, content, 'utf-8')
    return { success: true, path: result.filePath }
  }
  return { success: false }
})

// 读取文件
ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const content = await fs.readFile(filePath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 列出目录
ipcMain.handle('list-directory', async (event, dirPath) => {
  try {
    const files = await fs.readdir(dirPath, { withFileTypes: true })
    return files.map(file => ({
      name: file.name,
      isDirectory: file.isDirectory(),
      path: path.join(dirPath, file.name)
    }))
  } catch (error) {
    return []
  }
})

系统托盘

javascript
const { Tray, Menu, nativeImage } = require('electron')

let tray = null

function createTray() {
  // 创建托盘图标
  const icon = nativeImage.createFromPath(path.join(__dirname, 'tray-icon.png'))
  tray = new Tray(icon.resize({ width: 16, height: 16 }))
  
  // 设置提示文本
  tray.setToolTip('My Electron App')
  
  // 创建右键菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示窗口',
      click: () => {
        if (mainWindow) {
          mainWindow.show()
        }
      }
    },
    {
      label: '隐藏窗口',
      click: () => {
        if (mainWindow) {
          mainWindow.hide()
        }
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])
  
  tray.setContextMenu(contextMenu)
  
  // 单击托盘图标
  tray.on('click', () => {
    if (mainWindow) {
      if (mainWindow.isVisible()) {
        mainWindow.hide()
      } else {
        mainWindow.show()
      }
    }
  })
}

app.whenReady().then(() => {
  createTray()
})

系统通知

javascript
const { Notification } = require('electron')

// 检查是否支持通知
if (Notification.isSupported()) {
  // 创建通知
  const notification = new Notification({
    title: '新消息',
    body: '您有一条新消息',
    icon: path.join(__dirname, 'icon.png'),
    silent: false,
    urgency: 'normal'  // low, normal, critical
  })
  
  // 显示通知
  notification.show()
  
  // 通知点击事件
  notification.on('click', () => {
    console.log('通知被点击')
    if (mainWindow) {
      mainWindow.show()
    }
  })
}

// 从渲染进程触发通知
ipcMain.on('show-notification', (event, { title, body }) => {
  new Notification({ title, body }).show()
})

全局快捷键

javascript
const { globalShortcut } = require('electron')

app.whenReady().then(() => {
  // 注册全局快捷键
  globalShortcut.register('CommandOrControl+Shift+K', () => {
    console.log('快捷键被触发')
    if (mainWindow) {
      mainWindow.show()
    }
  })
  
  // 检查快捷键是否注册成功
  const isRegistered = globalShortcut.isRegistered('CommandOrControl+Shift+K')
  console.log('快捷键注册状态:', isRegistered)
})

// 应用退出时注销快捷键
app.on('will-quit', () => {
  globalShortcut.unregisterAll()
})

剪贴板操作

javascript
const { clipboard } = require('electron')

// 读取剪贴板文本
ipcMain.handle('clipboard-read-text', () => {
  return clipboard.readText()
})

// 写入剪贴板文本
ipcMain.handle('clipboard-write-text', (event, text) => {
  clipboard.writeText(text)
  return true
})

// 读取剪贴板图片
ipcMain.handle('clipboard-read-image', () => {
  const image = clipboard.readImage()
  if (!image.isEmpty()) {
    return image.toDataURL()
  }
  return null
})

// 写入剪贴板图片
ipcMain.handle('clipboard-write-image', (event, dataURL) => {
  const image = nativeImage.createFromDataURL(dataURL)
  clipboard.writeImage(image)
  return true
})

屏幕截图

javascript
const { desktopCapturer } = require('electron')

// 获取屏幕源
ipcMain.handle('get-sources', async () => {
  const sources = await desktopCapturer.getSources({
    types: ['screen', 'window'],
    thumbnailSize: { width: 1920, height: 1080 }
  })
  
  return sources.map(source => ({
    id: source.id,
    name: source.name,
    thumbnail: source.thumbnail.toDataURL()
  }))
})

// 渲染进程截图
// preload.js
contextBridge.exposeInMainWorld('captureAPI', {
  getSources: () => ipcRenderer.invoke('get-sources')
})

// renderer.js
async function captureScreen() {
  const sources = await window.captureAPI.getSources()
  
  // 选择第一个屏幕
  const source = sources[0]
  
  // 使用 getUserMedia 捕获屏幕
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      mandatory: {
        chromeMediaSource: 'desktop',
        chromeMediaSourceId: source.id
      }
    }
  })
  
  // 创建 video 元素
  const video = document.createElement('video')
  video.srcObject = stream
  video.play()
  
  // 截图
  video.onloadedmetadata = () => {
    const canvas = document.createElement('canvas')
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight
    
    const ctx = canvas.getContext('2d')
    ctx.drawImage(video, 0, 0)
    
    // 获取图片数据
    const dataURL = canvas.toDataURL('image/png')
    
    // 停止流
    stream.getTracks().forEach(track => track.stop())
    
    return dataURL
  }
}

菜单与快捷键

应用菜单

javascript
const { Menu, app } = require('electron')

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: () => {
          console.log('新建文件')
        }
      },
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: async () => {
          const result = await dialog.showOpenDialog({
            properties: ['openFile']
          })
          console.log(result.filePaths)
        }
      },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        click: () => {
          console.log('保存文件')
        }
      },
      { type: 'separator' },
      {
        label: '退出',
        accelerator: 'CmdOrCtrl+Q',
        role: 'quit'
      }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
      { label: '重做', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
      { type: 'separator' },
      { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
      { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
      { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' },
      { label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }
    ]
  },
  {
    label: '视图',
    submenu: [
      { label: '重新加载', accelerator: 'CmdOrCtrl+R', role: 'reload' },
      { label: '强制重新加载', accelerator: 'CmdOrCtrl+Shift+R', role: 'forceReload' },
      { label: '开发者工具', accelerator: 'CmdOrCtrl+Shift+I', role: 'toggleDevTools' },
      { type: 'separator' },
      { label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
      { label: '放大', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
      { label: '缩小', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
      { type: 'separator' },
      { label: '全屏', accelerator: 'F11', role: 'togglefullscreen' }
    ]
  },
  {
    label: '帮助',
    submenu: [
      {
        label: '关于',
        click: () => {
          dialog.showMessageBox({
            type: 'info',
            title: '关于',
            message: 'My Electron App',
            detail: 'Version 1.0.0'
          })
        }
      },
      {
        label: '检查更新',
        click: () => {
          console.log('检查更新')
        }
      }
    ]
  }
]

// macOS 特殊处理:添加应用名称菜单
if (process.platform === 'darwin') {
  template.unshift({
    label: app.name,
    submenu: [
      { role: 'about' },
      { type: 'separator' },
      { role: 'services' },
      { type: 'separator' },
      { role: 'hide' },
      { role: 'hideOthers' },
      { role: 'unhide' },
      { type: 'separator' },
      { role: 'quit' }
    ]
  })
}

const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

上下文菜单

javascript
// 渲染进程
window.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  window.electronAPI.showContextMenu()
})

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  showContextMenu: () => ipcRenderer.send('show-context-menu')
})

// 主进程
const { Menu } = require('electron')

ipcMain.on('show-context-menu', (event) => {
  const template = [
    {
      label: '复制',
      role: 'copy'
    },
    {
      label: '粘贴',
      role: 'paste'
    },
    { type: 'separator' },
    {
      label: '刷新',
      click: () => {
        event.sender.reload()
      }
    }
  ]
  
  const menu = Menu.buildFromTemplate(template)
  menu.popup(BrowserWindow.fromWebContents(event.sender))
})

打包与分发

使用 electron-builder

1. 安装

bash
npm install electron-builder --save-dev

2. 配置 package.json

json
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder",
    "build:win": "electron-builder --win",
    "build:mac": "electron-builder --mac",
    "build:linux": "electron-builder --linux"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "My Electron App",
    "directories": {
      "output": "dist"
    },
    "files": [
      "main.js",
      "preload.js",
      "renderer.js",
      "index.html",
      "package.json"
    ],
    "win": {
      "target": ["nsis", "portable"],
 Windows 图标(256x256)
├── icon.icns     # macOS 图标
└── icon.png      # Linux 图标(512x512)

4. 打包

bash
# 打包当前平台
npm run build

# 打包 Windows
npm run build:win

# 打包 macOS
npm run build:mac

# 打包 Linux
npm run build:linux

多平台打包配置

json
{
  "build": {
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64", "ia32"]
        },
        {
          "target": "portable",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.ico",
      "requestedExecutionLevel": "asInvoker"
    },
    "mac": {
      "target": [
        {
          "target": "dmg",
          "arch": ["x64", "arm64"]
        },
        {
          "target": "zip",
          "arch": ["x64", "arm64"]
        }
      ],
      "icon": "build/icon.icns",
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist"
    },
    "linux": {
      "target": ["AppImage", "deb", "rpm"],
      "icon": "build/icons",
      "category": "Utility",
      "maintainer": "your-email@example.com"
    }
  }
}

代码签名

Windows 签名

json
{
  "build": {
    "win": {
      "certificateFile": "path/to/certificate.pfx",
      "certificatePassword": "password",
      "signingHashAlgorithms": ["sha256"],
      "sign": "./customSign.js"
    }
  }
}

macOS 签名

json
{
  "build": {
    "mac": {
      "identity": "Developer ID Application: Your Name (TEAM_ID)",
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "build/entitlements.mac.plist"
    },
    "afterSign": "scripts/notarize.js"
  }
}

notarize.js(macOS 公证):

javascript
const { notarize } = require('electron-notarize')

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context
  
  if (electronPlatformName !== 'darwin') {
    return
  }
  
  const appName = context.packager.appInfo.productFilename
  
  return await notarize({
    appBundleId: 'com.example.myapp',
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_ID_PASSWORD,
    teamId: process.env.APPLE_TEAM_ID
  })
}

自动更新

使用 electron-updater

1. 安装

bash
npm install electron-updater --save

2. 配置 package.json

json
{
  "build": {
    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "your-repo"
      }
    ]
  }
}

3. 主进程代码

javascript
const { autoUpdater } = require('electron-updater')
const log = require('electron-log')

// 配置日志
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'

// 检查更新
function checkForUpdates() {
  autoUpdater.checkForUpdatesAndNotify()
}

// 更新事件
autoUpdater.on('checking-for-update', () => {
  log.info('正在检查更新...')
  sendStatusToWindow('正在检查更新...')
})

autoUpdater.on('update-available', (info) => {
  log.info('发现新版本')
  sendStatusToWindow('发现新版本')
})

autoUpdater.on('update-not-available', (info) => {
  log.info('当前已是最新版本')
  sendStatusToWindow('当前已是最新版本')
})

autoUpdater.on('error', (err) => {
  log.error('更新错误:', err)
  sendStatusToWindow('更新错误: ' + err)
})

autoUpdater.on('download-progress', (progressObj) => {
  let message = `下载速度: ${progressObj.bytesPerSecond}`
  message += ` - 已下载 ${progressObj.percent}%`
  message += ` (${progressObj.transferred}/${progressObj.total})`
  
  log.info(message)
  sendStatusToWindow(message)
})

autoUpdater.on('update-downloaded', (info) => {
  log.info('更新下载完成')
  sendStatusToWindow('更新下载完成')
  
  // 提示用户重启应用
  dialog.showMessageBox({
    type: 'info',
    title: '安装更新',
    message: '更新已下载完成,是否立即重启应用?',
    buttons: ['立即重启', '稍后']
  }).then((result) => {
    if (result.response === 0) {
      autoUpdater.quitAndInstall()
    }
  })
})

function sendStatusToWindow(text) {
  if (mainWindow) {
    mainWindow.webContents.send('update-status', text)
  }
}

// 应用启动后检查更新
app.whenReady().then(() => {
  createWindow()
  
  // 延迟 3 秒后检查更新
  setTimeout(() => {
    checkForUpdates()
  }, 3000)
})

// 手动检查更新
ipcMain.on('check-for-updates', () => {
  checkForUpdates()
})

4. 渲染进程代码

javascript
// preload.js
contextBridge.exposeInMainWorld('updaterAPI', {
  checkForUpdates: () => ipcRenderer.send('check-for-updates'),
  onUpdateStatus: (callback) => ipcRenderer.on('update-status', (event, tex
itHub 仓库创建新的 Release
- 上传构建产物(.exe、.dmg、.AppImage 等)
- electron-updater 会自动检测新版本

### 灰度发布

```javascript
const { autoUpdater } = require('electron-updater')

// 配置更新通道
autoUpdater.channel = 'beta'  // stable, beta, alpha

// 或者根据用户配置
const userChannel = store.get('updateChannel', 'stable')
autoUpdater.channel = userChannel

// 检查更新
autoUpdater.checkForUpdates()

性能优化

启动优化

1. 延迟加载

javascript
// 主进程
app.whenReady().then(() => {
  // 立即创建主窗口
  createWindow()
  
  // 延迟加载其他功能
  setTimeout(() => {
    initTray()
    initAutoUpdater()
    initGlobalShortcuts()
  }, 1000)
})

2. 预加载优化

javascript
// 只暴露必要的 API
contextBridge.exposeInMainWorld('electronAPI', {
  // 核心 API
  platform: process.platform,
  
  // 按需加载的 API
  loadFileAPI: () => {
    return {
      selectFile: () => ipcRenderer.invoke('select-file'),
      readFile: (path) => ipcRenderer.invoke('read-file', path)
    }
  }
})

3. 窗口显示优化

javascript
const win = new BrowserWindow({
  show: false,  // 创建时不显示
  backgroundColor: '#ffffff'  // 设置背景色避免闪烁
})

// 页面加载完成后显示
win.once('ready-to-show', () => {
  win.show()
})

内存优化

1. 及时释放资源

javascript
// 关闭窗口时释放资源
win.on('closed', () => {
  win = null
  // 清理其他资源
  clearInterval(timer)
  removeAllListeners()
})

2. 限制渲染进程数量

javascript
// 复用窗口而不是创建新窗口
function showWindow() {
  if (win) {
    win.show()
  } else {
    createWindow()
  }
}

3. 使用 webContents.session 清理缓存

javascript
// 清理缓存
app.on('before-quit', async () => {
  const session = require('electron').session
  await session.defaultSession.clearCache()
  await session.defaultSession.clearStorageData()
})

渲染优化

1. 虚拟列表

javascript
// 使用 react-window 或 vue-virtual-scroller
import { FixedSizeList } from 'react-window'

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index]}
        </div>
      )}
    </FixedSizeList>
  )
}

2. 懒加载图片

javascript
// 使用 Intersection Observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      observer.unobserve(img)
    }
  })
})

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img)
})

打包体积优化

1. 排除不必要的文件

json
{
  "build": {
    "files": [
      "main.js",
      "preload.js",
      "renderer/**/*",
      "!node_modules/**/*",
      "node_modules/electron-store/**/*"
    ],
    "asarUnpack": [
      "node_modules/native-module/**/*"
    ]
  }
}

2. 使用 asar 压缩

json
{
  "build": {
    "asar": true,
    "asarUnpack": [
      "**/*.node"
    ]
  }
}

3. 代码分割

javascript
// 使用动态导入
button.addEventListener('click', async () => {
  const module = await import('./heavy-module.js')
  module.doSomething()
})

安全最佳实践

核心安全配置

javascript
const win = new BrowserWindow({
  webPreferences: {
    // 1. 启用上下文隔离(必须)
    contextIsolation: true,
    
    // 2. 禁用 Node.js 集成(必须)
    nodeIntegration: false,
    
    // 3. 启用沙箱(推荐)
    sandbox: true,
    
    // 4. 禁用远程模块(已废弃)
    enableRemoteModule: false,
    
    // 5. 使用预加载脚本
    preload: path.join(__dirname, 'preload.js'),
    
    // 6. 禁用 webview 标签
    webviewTag: false
  }
})

CSP(内容安全策略)

html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self'; 
               style-src 'self' 'unsafe-inline'; 
               img-src 'self' data: https:;">
javascript
// 或在主进程设置
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ["default-src 'self'"]
    }
  })
})

安全的 IPC 通信

javascript
// ❌ 不安全:直接暴露 ipcRenderer
window.ipcRenderer = require('electron').ipcRenderer

// ✅ 安全:通过 contextBridge 暴露特定 API
contextBridge.exposeInMainWorld('electronAPI', {
  // 只暴露需要的方法
  readFile: (path) => {
    // 验证路径
    if (!isValidPath(path)) {
      throw new Error('Invalid path')
    }
    return ipcRenderer.invoke('read-file', path)
  }
})

// 主进程验证
ipcMain.handle('read-file', async (event, filePath) => {
  // 验证来源
  if (!isValidSender(event.sender)) {
    throw new Error('Unauthorized')
  }
  
  // 验证路径
  if (!isValidPath(filePath)) {
    throw new Error('Invalid path')
  }
  
  // 读取文件
  return await fs.readFile(filePath, 'utf-8')
})

防止远程内容注入

javascript
// 加载远程内容时的安全措施
win.loadURL('https://example.com', {
  userAgent: 'MyApp/1.0.0',
  extraHeaders: 'X-Custom-Header: value'
})

// 监听导航事件
win.webContents.on('will-navigate', (event, url) => {
  // 只允许特定域名
  const allowedDomains = ['https://example.com']
  const parsedUrl = new URL(url)
  
  if (!allowedDomains.includes(parsedUrl.origin)) {
    event.preventDefault()
    console.warn('Blocked navigation to:', url)
  }
})

// 阻止新窗口打开
win.webContents.setWindowOpenHandler(({ url }) => {
  // 在默认浏览器中打开
  shell.openExternal(url)
  return { action: 'deny' }
})

输入验证

javascript
// 主进程
ipcMain.handle('save-data', async (event, data) => {
  // 验证数据类型
  if (typeof data !== 'object') {
    throw new Error('Invalid data type')
  }
  
  // 验证必填字段
  if (!data.name || !data.email) {
    throw new Error('Missing required fields')
  }
  
  // 验证邮箱格式
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!emailRegex.test(data.email)) {
    throw new Error('Invalid email format')
  }
  
  // 清理输入(防止 XSS)
  const cleanData = {
    name: sanitizeHtml(data.name),
    email: sanitizeHtml(data.email)
  }
  
  // 保存数据
  await saveToDatabase(cleanData)
})

调试技巧

渲染进程调试

1. 使用 DevTools

javascript
// 主进程
const win = new BrowserWindow({
  webPreferences: {
    devTools: true  // 启用 DevTools
  }
})

// 打开 DevTools
win.webContents.openDevTools()

// 分离 DevTools
win.webContents.openDevTools({ mode: 'detach' })

// 关闭 DevTools
win.webContents.closeDevTools()

2. 使用 Vue/React DevTools

bash
# 安装 electron-devtools-installer
npm install electron-devtools-installer --save-dev
javascript
// 主进程
const { default: installExtension, REACT_DEVELOPER_TOOLS, VUEJS_DEVTOOLS } = require('electron-devtools-installer')

app.whenReady().then(() => {
  // 安装 React DevTools
  installExtension(REACT_DEVELOPER_TOOLS)
    .then((name) => console.log(`Added Extension: ${name}`))
    .catch((err) => console.log('An error occurred: ', err))
  
  // 安装 Vue DevTools
  installExtension(VUEJS_DEVTOOLS)
    .then((name) => console.log(`Added Extension: ${name}`))
    .catch((err) => console.log('An error occurred: ', err))
})

主进程调试

1. 使用 VS Code 调试

.vscode/launch.json

json
{
  "version": "0.2.0",
  "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"
    }
  ]
}

2. 使用 Chrome DevTools

bash
# 启动时添加 --inspect 参数
electron --inspect=5858 .

# 或在代码中启用
# main.js
if (process.env.NODE_ENV === 'development') {
  require('electron').app.commandLine.appendSwitch('inspect', '5858')
}

然后在 Chrome 中访问 chrome://inspect

日志系统

javascript
// 使用 electron-log
const log = require('electron-log')

// 配置日志
log.transports.file.level = 'info'
log.transports.file.maxSize = 5 * 1024 * 1024  // 5MB
log.transports.console.level = 'debug'

// 记录日志
log.info('应用启动')
log.warn('警告信息')
log.error('错误信息', error)

// 查看日志文件位置
console.log(log.transports.file.getFile().path)
// Windows: %USERPROFILE%\AppData\Roaming\<app name>\logs\main.log
// macOS: ~/Library/Logs/<app name>/main.log
// Linux: ~/.config/<app name>/logs/main.log

崩溃报告

javascript
const { crashReporter } = require('electron')

// 启动崩溃报告
crashReporter.start({
  productName: 'MyApp',
  companyName: 'MyCompany',
  submitURL: 'https://your-crash-report-server.com/submit',
  uploadToServer: true
})

// 主进程崩溃处理
process.on('uncaughtException', (error) => {
  log.error('Uncaught Exception:', error)
  // 显示错误对话框
  dialog.showErrorBox('应用错误', error.message)
})

// 渲染进程崩溃处理
win.webContents.on('crashed', (event, killed) => {
  log.error('Renderer process crashed')
  
  // 重新加载页面
  const options = {
    type: 'error',
    title: '应用崩溃',
    message: '应用遇到了一个错误,是否重新加载?',
    buttons: ['重新加载', '退出']
  }
  
  dialog.showMessageBox(options).then((result) => {
    if (result.response === 0) {
      win.reload()
    } else {
      app.quit()
    }
  })
})

实战案例

案例 1:Markdown 编辑器

功能需求

  • 文件打开/保存
  • 实时预览
  • 语法高亮
  • 导出 PDF

项目结构

markdown-editor/
├── main.js
├── preload.js
├── src/
│   ├── index.html
│   ├── editor.js
│   ├── preview.js
│   └── styles.css
└── package.json

核心代码

javascript
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const fs = require('fs').promises
const path = require('path')

let mainWindow = null
let currentFilePath = null

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })
  
  mainWindow.loadFile('src/index.html')
}

// 打开文件
ipcMain.handle('open-file', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Markdown', extensions: ['md', 'markdown'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  })
  
  if (!result.canceled) {
    currentFilePath = result.filePaths[0]
    const content = await fs.readFile(currentFilePath, 'utf-8')
    return { success: true, content, path: currentFilePath }
  }
  return { success: false }
})

// 保存文件
ipcMain.handle('save-file', async (event, content) => {
  if (currentFilePath) {
    await fs.writeFile(currentFilePath, content, 'utf-8')
    return { success: true, path: currentFilePath }
  } else {
    return await saveFileAs(content)
  }
})

// 另存为
ipcMain.handle('save-file-as', async (event, content) => {
  return await saveFileAs(content)
})

async function saveFileAs(content) {
  const result = await dialog.showSaveDialog({
    defaultPath: 'untitled.md',
    filters: [
      { name: 'Markdown', extensions: ['md'] }
    ]
  })
  
  if (!result.canceled) {
    currentFilePath = result.filePath
    await fs.writeFile(currentFilePath, content, 'utf-8')
ateWindow)
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('editorAPI', {
  openFile: () => ipcRenderer.invoke('open-file'),
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
  saveFileAs: (content) => ipcRenderer.invoke('save-file-as', content),
  exportPDF: () => ipcRenderer.invoke('export-pdf')
})
html
<!-- src/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Markdown Editor</title>
  <link rel="stylesheet" href="styles.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/marked/marked.min.css">
</head>
<body>
  <div class="toolbar">
    <button id="open-btn">打开</button>
    <button id="save-btn">保存</button>
    <button id="save-as-btn">另存为</button>
    <button id="export-pdf-btn">导出 PDF</button>
  </div>
  
  <div class="container">
    <div class="editor-pane">
      <textarea id="editor" placeholder="在此输入 Markdown..."></textarea>
    </div>
    <div class="preview-pane">
      <div id="preview"></div>
    </div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="editor.js"></script>
</body>
</html>
javascript
// src/editor.js
const editor = document.getElementById('editor')
const preview = document.getElementById('preview')

// 实时预览
editor.addEventListener('input', () => {
  const markdown = editor.value
  const html = marked.parse(markdown)
  preview.innerHTML = html
})

// 打开文件
document.getElementById('open-btn').addEventListener('click', async () => {
  const result = await window.editorAPI.openFile()
  if (result.success) {
    editor.value = result.content
    editor.dispatchEvent(new Event('input'))
  }
})

// 保存文件
document.getElementById('save-btn').addEventListener('click', async () => {
  const content = editor.value
  const result = await window.editorAPI.saveFile(content)
  if (result.success) {
    alert('保存成功')
  }
})

// 另存为
document.getElementById('save-as-btn').addEventListener('click', async () => {
  const content = editor.value
  const result = await window.editorAPI.saveFileAs(content)
  if (result.success) {
    alert('保存成功')
  }
})

// 导出 PDF
document.getElementById('export-pdf-btn').addEventListener('click', async () => {
  const result = await window.editorAPI.exportPDF()
  if (result.success) {
    alert('导出成功')
  }
})

案例 2:系统监控工具

功能需求

  • CPU 使用率
  • 内存使用率
  • 磁盘使用率
  • 网络流量
  • 系统托盘显示

核心代码

javascript
// main.js
const { app, BrowserWindow, Tray, Menu } = require('electron')
const os = require('os')
const si = require('systeminformation')

let mainWindow = null
let tray = null

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 400,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  
  mainWindow.loadFile('index.html')
}

function createTray() {
  tray = new Tray(path.join(__dirname, 'icon.png'))
  
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示', click: () => mainWindow.show() },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setContextMenu(contextMenu)
  tray.setToolTip('系统监控')
}

// 获取系统信息
ipcMain.handle('get-system-info', async () => {
  const [cpu, mem, disk, network] = await Promise.all([
    si.currentLoad(),
    si.mem(),
    si.fsSize(),
    si.networkStats()
  ])
  
  return {
    cpu: {
      usage: cpu.currentLoad.toFixed(2),
      cores: os.cpus().length
    },
    memory: {
      total: (mem.total / 1024 / 1024 / 1024).toFixed(2),
      used: (mem.used / 1024 / 1024 / 1024).toFixed(2),
      usage: ((mem.used / mem.total) * 100).toFixed(2)
    },
    disk: disk.map(d => ({
      fs: d.fs,
      size: (d.size / 1024 / 1024 / 1024).toFixed(2),
      used: (d.used / 1024 / 1024 / 1024).toFixed(2),
      usage: d.use.toFixed(2)
    })),
    network: network.map(n => ({
      iface: n.iface,
      rx: (n.rx_sec / 1024).toFixed(2),
      tx: (n.tx_sec / 1024).toFixed(2)
    }))
  }
})

app.whenReady().then(() => {
  createWindow()
  createTray()
})

常见问题

1. 白屏问题

原因

  • 页面加载失败
  • 路径错误
  • CSP 配置过严

解决方案

javascript
// 1. 检查路径
win.loadFile('index.html')  // 相对路径
win.loadURL(`file://${__dirname}/index.html`)  // 绝对路径

// 2. 监听加载事件
win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
  console.error('页面加载失败:', errorDescription)
})

// 3. 延迟显示窗口
win.once('ready-to-show', () => {
  win.show()
})

2. 打包后无法运行

原因

  • 路径问题(开发环境 vs 生产环境)
  • 原生模块未正确打包
  • 缺少依赖

解决方案

javascript
// 1. 使用正确的路径
const isDev = process.env.NODE_ENV === 'development'
const preloadPath = isDev
  ? path.join(__dirname, 'preload.js')
  : path.join(process.resourcesPath, 'app.asar', 'preload.js')

// 2. 配置 asar 解包
{
  "build": {
    "asarUnpack": [
      "node_modules/native-module/**/*"
    ]
  }
}

// 3. 使用 extraResources
{
  "build": {
    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/"
      }
    ]
  }
}

3. IPC 通信失败

原因

  • 通道名称不匹配
  • 未正确注册监听器
  • 上下文隔离问题

解决方案

javascript
// 1. 确保通道名称一致
// 渲染进程
ipcRenderer.send('my-channel', data)

// 主进程
ipcMain.on('my-channel', (event, data) => {
  // 处理
})

// 2. 使用 invoke/handle(推荐)
// 渲染进程
const result = await ipcRenderer.invoke('my-channel', data)

// 主进程
ipcMain.handle('my-channel', async (event, data) => {
  return result
})

// 3. 检查 contextBridge
contextBridge.exposeInMainWorld('api', {
  send: (channel, data) => ipcRenderer.send(channel, data)
})

4. 内存泄漏

原因

  • 未移除事件监听器
  • 循环引用
  • 大对象未释放

解决方案

javascript
// 1. 及时移除监听器
const handler = () => {}
ipcMain.on('event', handler)
// 使用完后移除
ipcMain.removeListener('event', handler)

// 2. 使用 once 代替 on
ipcMain.once('event', handler)

// 3. 清理窗口引用
win.on('closed', () => {
  win = null
})

// 4. 使用 WeakMap/WeakSet
const cache = new WeakMap()

面试要点

基础概念

Q1:什么是 Electron?它的架构是怎样的?

核心答案

  • Electron 是使用 Web 技术构建跨平台桌面应用的框架
  • 基于 Chromium(渲染引擎)和 Node.js(运行时)
  • 采用多进程架构:主进程 + 多个渲染进程
  • 主进程管理应用生命周期和窗口,渲染进程负责页面渲染

追问点

  • 主进程和渲染进程的区别?
  • 为什么要使用多进程架构?
  • Electron 的优缺点?

Q2:Electron 的 IPC 通信机制是什么?

核心答案

  • IPC(Inter-Process Communication)用于主进程和渲染进程通信
  • 主要方式:
    • ipcRenderer.send + ipcMain.on(单向)
    • ipcRenderer.invoke + ipcMain.handle(双向,推荐)
    • webContents.send(主进程 → 渲染进程)
  • 通过预加载脚本和 contextBridge 暴露安全的 API

追问点

  • 如何保证 IPC 通信的安全性?
  • MessagePort 是什么?
  • 如何实现渲染进程之间的通信?

安全性

Q3:Electron 应用有哪些安全风险?如何防范?

核心答案

安全风险

  1. XSS 攻击
  2. 远程代码执行
  3. 原生能力滥用
  4. 中间人攻击

防范措施

  1. 启用 contextIsolation
  2. 禁用 nodeIntegration
  3. 启用 sandbox
  4. 配置 CSP
  5. 验证所有输入
  6. 使用 HTTPS
  7. 代码签名

代码示例

javascript
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    preload: path.join(__dirname, 'preload.js')
  }
})

追问点

  • contextIsolation 的作用?
  • 如何安全地暴露 Node.js API?
  • 如何防止 XSS 攻击?

性能优化

Q4:如何优化 Electron 应用的性能?

核心答案

启动优化

  1. 延迟加载非核心功能
  2. 使用 show: false + ready-to-show
  3. 预加载脚本优化

内存优化

  1. 及时释放资源
  2. 限制渲染进程数量
  3. 使用 WeakMap/WeakSet
  4. 清理缓存

渲染优化

  1. 虚拟列表
  2. 懒加载图片
  3. 代码分割
  4. 使用 Web Workers

打包优化

  1. 排除不必要的文件
  2. 使用 asar 压缩
  3. 代码压缩和混淆

追问点

  • 如何排查内存泄漏?
  • 如何减小打包体积?
  • 如何监控应用性能?

打包与分发

Q5:如何打包和分发 Electron 应用?

核心答案

打包工具

  • electron-builder(推荐)
  • electron-packager
  • electron-forge

多平台打包

json
{
  "build": {
    "win": { "target": ["nsis", "portable"] },
    "mac": { "target": ["dmg", "zip"] },
    "linux": { "target": ["AppImage", "deb"] }
  }
}

代码签名

  • Windows:使用 .pfx 证书
  • macOS:使用 Developer ID + 公证
  • Linux:不强制要求

自动更新

  • 使用 electron-updater
  • 配置更新服务器(GitHub Releases)
  • 实现增量更新

追问点

  • 如何实现自动更新?
  • 如何进行代码签名?
  • 如何实现灰度发布?

实战场景

Q6:如何实现一个文件管理器?

核心答案

功能模块

  1. 文件浏览(fs.readdir)
  2. 文件操作(复制、移动、删除)
  3. 文件预览(图片、文本、PDF)
  4. 搜索功能
  5. 右键菜单

关键技术

javascript
// 1. 列出目录
ipcMain.handle('list-directory', async (event, dirPath) => {
  const files = await fs.readdir(dirPath, { withFileTypes: true })
  return files.map(file => ({
    name: file.name,
    isDirectory: file.isDirectory(),
    size: file.isFile() ? (await fs.stat(path.join(dirPath, fi

3. 使用 Canvas 绘制选区
4. 截取选区图片
5. 保存或复制到剪贴板

**关键代码**
```javascript
// 1. 获取屏幕源
const sources = await desktopCapturer.getSources({
  types: ['screen'],
  thumbnailSize: { width: 1920, height: 1080 }
})

// 2. 创建截图窗口
const captureWin = new BrowserWindow({
  fullscreen: true,
  transparent: true,
  frame: false,
  alwaysOnTop: true
})

// 3. 截图并保存
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.drawImage(video, x, y, width, height, 0, 0, width, height)
const dataURL = canvas.toDataURL('image/png')

追问点

  • 如何实现多显示器截图?
  • 如何添加标注功能?
  • 如何优化截图性能?

参考资料

官方资源

学习教程

常用库

开发工具

  • electron-builder(打包)
  • electron-updater(自动更新)
  • electron-store(数据持久化)
  • electron-log(日志)
  • electron-devtools-installer(DevTools 扩展)

UI 框架

  • React + Electron
  • Vue + Electron
  • Angular + Electron
  • Svelte + Electron

实用库

  • systeminformation(系统信息)
  • node-notifier(通知)
  • chokidar(文件监听)
  • sharp(图片处理)

开源项目

知名应用

示例项目

社区资源

学习路线

初级(1-2 周)

  1. 了解 Electron 架构
  2. 创建第一个应用
  3. 学习 IPC 通信
  4. 掌握窗口管理

中级(2-4 周)

  1. 原生能力调用
  2. 菜单与快捷键
  3. 打包与分发
  4. 自动更新

高级(1-2 月)

  1. 性能优化
  2. 安全最佳实践
  3. 调试技巧
  4. 实战项目

总结

Electron 是一个强大的跨平台桌面应用开发框架,它让前端开发者能够使用熟悉的 Web 技术构建桌面应用。掌握 Electron 需要理解其多进程架构、IPC 通信机制、安全最佳实践和性能优化策略。

核心要点

  1. ✅ 理解主进程和渲染进程的区别
  2. ✅ 掌握 IPC 通信的各种方式
  3. ✅ 遵循安全最佳实践(contextIsolation、nodeIntegration)
  4. ✅ 学会使用原生能力(文件系统、系统托盘、通知等)
  5. ✅ 掌握打包和分发流程
  6. ✅ 实现自动更新机制
  7. ✅ 进行性能优化和调试

通过本指南的学习和实践,你将能够独立开发和发布专业的 Electron 桌面应用!

基于 VitePress 构建