📱 Electron 面试题大全

📚 目录

  1. 基础概念
  2. 进程架构
  3. 进程通信 IPC
  4. 窗口管理
  5. 安全性
  6. 性能优化
  7. 打包部署
  8. 实战场景

🎯 基础概念

Q1: 什么是 Electron?它的核心优势是什么?

定义:
Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的框架,由 GitHub 开发。

核心组成:

Electron = Chromium + Node.js + Native APIs
           ↓          ↓         ↓
         渲染引擎   后端能力   系统调用

主要优势:

  1. 跨平台开发
// 一套代码,三个平台
const app = {
  platforms: ['Windows', 'macOS', 'Linux'],
  code: 'Write once, run everywhere'
}
  1. Web 技术栈
// 前端开发者可以直接上手
import React from 'react'
import { ipcRenderer } from 'electron'

function App() {
  const handleClick = () => {
    ipcRenderer.send('native-action')
  }
  return <button onClick={handleClick}>调用系统 API</button>
}
  1. 强大的生态系统
# 可以使用所有 npm 包
npm install axios lodash electron-store
  1. 原生能力
// 访问文件系统
const fs = require('fs')
const path = require('path')

// 调用系统通知
const { Notification } = require('electron')
new Notification({ title: '提示', body: '操作完成' }).show()

劣势:

典型应用:


Q2: Electron 的架构是怎样的?

双进程架构:

┌─────────────────────────────────────────┐
│         主进程 (Main Process)            │
│  • Node.js 环境                          │
│  • 控制应用生命周期                      │
│  • 创建和管理窗口                        │
│  • 调用系统 API                          │
│  • 文件: main.js / background.js         │
└─────────────┬───────────────────────────┘
              │
              │ 创建
              ↓
┌─────────────────────────────────────────┐
│      渲染进程 (Renderer Process)         │
│  • Chromium 浏览器环境                   │
│  • 每个窗口一个独立进程                  │
│  • 运行前端代码 (HTML/CSS/JS)            │
│  • 文件: index.html, renderer.js         │
└─────────────────────────────────────────┘

详细示例:

// ============ 主进程 main.js ============
const { app, BrowserWindow } = require('electron')
const path = require('path')

let mainWindow

// 应用启动
app.whenReady().then(() => {
  createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

// 创建窗口
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false  // 安全考虑
    }
  })

  mainWindow.loadFile('index.html')
}

// 所有窗口关闭时退出(Windows & Linux)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})


// ============ 渲染进程 renderer.js ============
// 只能使用 Web APIs 和通过 preload 暴露的 API
document.getElementById('btn').addEventListener('click', () => {
  // ❌ 不能直接使用 Node.js API(如果 nodeIntegration: false)
  // const fs = require('fs')  // 报错

  // ✅ 通过 IPC 与主进程通信
  window.electronAPI.sendMessage('Hello from renderer')
})


// ============ 预加载脚本 preload.js ============
const { contextBridge, ipcRenderer } = require('electron')

// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  sendMessage: (msg) => ipcRenderer.send('message', msg),
  onReply: (callback) => ipcRenderer.on('reply', callback)
})

进程对比:

特性 主进程 渲染进程
数量 1 个 多个(每个窗口 1 个)
环境 Node.js Chromium + 有限的 Node.js
能力 完整的系统访问 受限的系统访问
生命周期 控制整个应用 依附于窗口
典型任务 窗口管理、系统交互 UI 渲染、用户交互

🔄 进程通信

Q3: Electron 中主进程和渲染进程如何通信?有哪些方式?

核心方式:IPC (Inter-Process Communication)

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

// ========== 预加载脚本 preload.js ==========
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  // 发送消息(不等待返回)
  saveFile: (data) => ipcRenderer.send('save-file', data)
})


// ========== 渲染进程 renderer.js ==========
document.getElementById('save').addEventListener('click', () => {
  window.electronAPI.saveFile({ content: 'Hello World' })
})


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

ipcMain.on('save-file', (event, data) => {
  console.log('收到数据:', data)
  fs.writeFileSync('output.txt', data.content)
})

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

// ========== 预加载脚本 preload.js ==========
contextBridge.exposeInMainWorld('electronAPI', {
  // 发送并等待回复
  readFile: (filename) => ipcRenderer.invoke('read-file', filename)
})


// ========== 渲染进程 renderer.js ==========
async function loadData() {
  try {
    const content = await window.electronAPI.readFile('data.json')
    console.log('文件内容:', content)
  } catch (error) {
    console.error('读取失败:', error)
  }
}


// ========== 主进程 main.js ==========
ipcMain.handle('read-file', async (event, filename) => {
  try {
    const content = fs.readFileSync(filename, 'utf-8')
    return content  // 返回给渲染进程
  } catch (error) {
    throw error
  }
})

3. 主进程 → 渲染进程(推送)

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

// 监听系统事件
powerMonitor.on('suspend', () => {
  // 向所有窗口推送消息
  BrowserWindow.getAllWindows().forEach(win => {
    win.webContents.send('system-event', { type: 'suspend' })
  })
})

// 向特定窗口推送
mainWindow.webContents.send('update-progress', { percent: 75 })


// ========== 预加载脚本 preload.js ==========
contextBridge.exposeInMainWorld('electronAPI', {
  onSystemEvent: (callback) => {
    ipcRenderer.on('system-event', (event, data) => callback(data))
  },
  onProgress: (callback) => {
    ipcRenderer.on('update-progress', (event, data) => callback(data))
  }
})


// ========== 渲染进程 renderer.js ==========
window.electronAPI.onSystemEvent((data) => {
  console.log('系统事件:', data.type)
})

window.electronAPI.onProgress((data) => {
  updateProgressBar(data.percent)
})

4. 渲染进程 ↔ 渲染进程(通过主进程中转)

// ========== 主进程 main.js ==========
const windows = {
  main: null,
  child: null
}

// 窗口 A 发送消息给窗口 B
ipcMain.on('send-to-child', (event, data) => {
  if (windows.child) {
    windows.child.webContents.send('message-from-main', data)
  }
})


// ========== 窗口 A (renderer-a.js) ==========
window.electronAPI.sendToChild({ msg: 'Hello Child' })


// ========== 窗口 B (renderer-b.js) ==========
window.electronAPI.onMessageFromMain((data) => {
  console.log('收到主窗口消息:', data.msg)
})

5. 使用 MessagePort (高级)

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

ipcMain.on('create-channel', (event) => {
  const { port1, port2 } = new MessageChannelMain()

  // 将 port1 发送给窗口 A
  event.sender.postMessage('port', null, [port1])

  // 将 port2 发送给窗口 B
  childWindow.webContents.postMessage('port', null, [port2])
})


// ========== 渲染进程(窗口 A & B)==========
window.addEventListener('message', (event) => {
  const [port] = event.ports

  // 直接通过 port 通信,不经过主进程
  port.onmessage = (e) => {
    console.log('收到消息:', e.data)
  }

  port.postMessage('Hello from renderer')
})

6. Remote 模块(已废弃,不推荐)

// ❌ Electron 14+ 已移除
const { BrowserWindow } = require('electron').remote
const win = new BrowserWindow()  // 不安全!

// ✅ 正确做法:使用 IPC
window.electronAPI.createWindow()

通信方式对比表

方式 方向 API 是否等待返回 使用场景
send/on 渲染 → 主 ipcRenderer.send
ipcMain.on
日志上报、后台任务
invoke/handle 渲染 → 主 → 渲染 ipcRenderer.invoke
ipcMain.handle
✅ (Promise) 文件读写、数据库查询
webContents.send 主 → 渲染 win.webContents.send 推送通知、进度更新
MessagePort 渲染 ↔ 渲染 MessageChannelMain 窗口间直接通信

最佳实践

// ✅ 好的实践
// 1. 使用 contextBridge,禁用 nodeIntegration
webPreferences: {
  contextIsolation: true,
  nodeIntegration: false,
  preload: path.join(__dirname, 'preload.js')
}

// 2. 统一管理 IPC 通道名
// ipc-channels.js
module.exports = {
  SAVE_FILE: 'save-file',
  READ_FILE: 'read-file',
  OPEN_DIALOG: 'open-dialog'
}

// 3. 错误处理
ipcMain.handle('risky-operation', async (event, data) => {
  try {
    return await doSomething(data)
  } catch (error) {
    console.error('操作失败:', error)
    throw error  // 传递给渲染进程
  }
})

// 4. 清理监听器
// 渲染进程
const cleanup = window.electronAPI.onProgress((data) => {
  // ...
})

// 组件卸载时
window.addEventListener('beforeunload', cleanup)

Q4: ipcRenderer.send 和 ipcRenderer.invoke 有什么区别?

核心区别:

特性 send/on invoke/handle
通信方式 单向(发送后不管) 双向(等待返回)
返回值 ❌ 无 ✅ Promise
主进程监听 ipcMain.on ipcMain.handle
错误处理 需要额外的回调 自动传递异常
使用场景 日志、通知 数据查询、文件操作

详细对比示例

方式 1: send/on(单向,无返回)

// ========== 渲染进程 ==========
// 发送日志(不需要等待)
window.electronAPI.log('用户点击了按钮')


// ========== 预加载脚本 ==========
contextBridge.exposeInMainWorld('electronAPI', {
  log: (message) => ipcRenderer.send('log', message)
})


// ========== 主进程 ==========
ipcMain.on('log', (event, message) => {
  console.log('[Renderer Log]', message)
  // 没有返回值
})

方式 2: invoke/handle(双向,有返回)

// ========== 渲染进程 ==========
async function loadUserData() {
  try {
    const user = await window.electronAPI.getUser(123)
    console.log('用户信息:', user)
  } catch (error) {
    console.error('获取失败:', error.message)
  }
}


// ========== 预加载脚本 ==========
contextBridge.exposeInMainWorld('electronAPI', {
  getUser: (id) => ipcRenderer.invoke('get-user', id)
})


// ========== 主进程 ==========
ipcMain.handle('get-user', async (event, userId) => {
  try {
    const user = await database.findUser(userId)
    return user  // ✅ 自动返回给渲染进程
  } catch (error) {
    throw new Error('用户不存在')  // ✅ 异常自动传递
  }
})

如果用 send/on 实现双向通信(不推荐)

// ❌ 老旧写法,代码复杂
// ========== 渲染进程 ==========
window.electronAPI.getUserOldWay(123, (error, user) => {
  if (error) {
    console.error(error)
  } else {
    console.log(user)
  }
})


// ========== 预加载脚本 ==========
contextBridge.exposeInMainWorld('electronAPI', {
  getUserOldWay: (id, callback) => {
    ipcRenderer.send('get-user-old', id)
    ipcRenderer.once('get-user-reply', (event, error, user) => {
      callback(error, user)
    })
  }
})


// ========== 主进程 ==========
ipcMain.on('get-user-old', async (event, userId) => {
  try {
    const user = await database.findUser(userId)
    event.sender.send('get-user-reply', null, user)  // ❌ 手动回复
  } catch (error) {
    event.sender.send('get-user-reply', error.message, null)
  }
})

问题:


使用建议

// ✅ 使用 send/on
// 1. 日志上报
ipcRenderer.send('log', { level: 'info', message: '...' })

// 2. 触发后台任务(不关心结果)
ipcRenderer.send('start-download', { url: '...' })

// 3. 通知主进程(不需要回复)
ipcRenderer.send('window-minimize')


// ✅ 使用 invoke/handle
// 1. 文件操作
const content = await ipcRenderer.invoke('read-file', 'data.json')

// 2. 数据库查询
const users = await ipcRenderer.invoke('query-users', { age: 18 })

// 3. 系统对话框
const result = await ipcRenderer.invoke('show-open-dialog', options)

// 4. 需要返回值的任何操作
const config = await ipcRenderer.invoke('get-app-config')

性能考虑

// ⚠️ 高频调用时的注意事项

// ❌ 不好:每次鼠标移动都调用 invoke
document.addEventListener('mousemove', async (e) => {
  await ipcRenderer.invoke('track-mouse', e.clientX, e.clientY)
  // 会造成大量 Promise 积压
})

// ✅ 好:高频事件用 send
document.addEventListener('mousemove', (e) => {
  ipcRenderer.send('track-mouse', e.clientX, e.clientY)
})

// ✅ 更好:使用节流
import { throttle } from 'lodash'

document.addEventListener('mousemove', throttle((e) => {
  ipcRenderer.send('track-mouse', e.clientX, e.clientY)
}, 100))

🪟 窗口管理

Q5: 如何创建不同类型的窗口(主窗口、子窗口、模态窗口)?

1. 主窗口

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

let mainWindow

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    title: '我的应用',

    // 窗口配置
    minWidth: 800,
    minHeight: 600,
    resizable: true,
    frame: true,  // 显示标题栏

    // Web 配置
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    },

    // 显示配置
    show: false,  // 先不显示,等加载完再显示
    backgroundColor: '#ffffff'
  })

  mainWindow.loadFile('index.html')

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

  // 窗口关闭事件
  mainWindow.on('closed', () => {
    mainWindow = null
  })
})

2. 子窗口

let childWindow

function createChildWindow() {
  childWindow = new BrowserWindow({
    width: 600,
    height: 400,
    parent: mainWindow,  // ✅ 设置父窗口
    modal: false,        // 非模态
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  childWindow.loadFile('child.html')

  childWindow.once('ready-to-show', () => {
    childWindow.show()
  })

  // 子窗口关闭时清理引用
  childWindow.on('closed', () => {
    childWindow = null
  })
}

// 主进程监听创建子窗口请求
ipcMain.on('open-child-window', () => {
  createChildWindow()
})

子窗口特性:


3. 模态窗口

let modalWindow

function createModalWindow() {
  modalWindow = new BrowserWindow({
    width: 500,
    height: 300,
    parent: mainWindow,  // 必须设置父窗口
    modal: true,         // ✅ 模态窗口
    show: false,
    frame: false,        // 无边框
    resizable: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  modalWindow.loadFile('modal.html')

  modalWindow.once('ready-to-show', () => {
    modalWindow.show()
  })
}

// 应用场景:确认对话框
ipcMain.handle('show-confirm-dialog', async (event, message) => {
  return new Promise((resolve) => {
    const dialog = new BrowserWindow({
      width: 400,
      height: 200,
      parent: BrowserWindow.fromWebContents(event.sender),
      modal: true,
      show: false
    })

    dialog.loadURL(`data:text/html,
      <h2>${message}</h2>
      <button onclick="window.close(); window.opener.postMessage('ok')">确定</button>
      <button onclick="window.close(); window.opener.postMessage('cancel')">取消</button>
    `)

    dialog.webContents.on('ipc-message', (e, channel, result) => {
      resolve(result === 'ok')
      dialog.close()
    })

    dialog.show()
  })
})

4. 无边框窗口

const framelessWindow = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,  // ✅ 无边框
  transparent: true,  // 透明背景
  webPreferences: {
    preload: path.join(__dirname, 'preload.js')
  }
})

framelessWindow.loadFile('frameless.html')

自定义标题栏 HTML:

<!-- frameless.html -->
<div class="titlebar">
  <div class="title">我的应用</div>
  <div class="controls">
    <button id="minimize"></button>
    <button id="maximize"></button>
    <button id="close">×</button>
  </div>
</div>

<style>
.titlebar {
  -webkit-app-region: drag;  /* ✅ 可拖动区域 */
  display: flex;
  justify-content: space-between;
  height: 32px;
  background: #2c3e50;
  color: white;
}

.controls button {
  -webkit-app-region: no-drag;  /* ✅ 按钮不可拖动 */
}
</style>

<script>
document.getElementById('minimize').onclick = () => {
  window.electronAPI.minimizeWindow()
}

document.getElementById('maximize').onclick = () => {
  window.electronAPI.maximizeWindow()
}

document.getElementById('close').onclick = () => {
  window.electronAPI.closeWindow()
}
</script>

主进程处理:

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

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

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

5. 托盘窗口

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

let tray
let trayWindow

app.whenReady().then(() => {
  // 创建托盘图标
  tray = new Tray(path.join(__dirname, 'icon.png'))

  // 点击托盘图标显示窗口
  tray.on('click', () => {
    if (!trayWindow) {
      trayWindow = new BrowserWindow({
        width: 300,
        height: 400,
        frame: false,
        show: false,
        skipTaskbar: true,  // ✅ 不显示在任务栏
        webPreferences: {
          preload: path.join(__dirname, 'preload.js')
        }
      })

      trayWindow.loadFile('tray.html')
    }

    // 计算位置(托盘图标下方)
    const bounds = tray.getBounds()
    const x = Math.round(bounds.x + (bounds.width / 2) - 150)
    const y = Math.round(bounds.y + bounds.height)

    trayWindow.setPosition(x, y)
    trayWindow.show()
  })

  // 失去焦点时隐藏
  if (trayWindow) {
    trayWindow.on('blur', () => {
      trayWindow.hide()
    })
  }
})

6. 多窗口管理

class WindowManager {
  constructor() {
    this.windows = new Map()
  }

  create(name, options) {
    if (this.windows.has(name)) {
      // 窗口已存在,聚焦
      this.windows.get(name).focus()
      return this.windows.get(name)
    }

    const win = new BrowserWindow(options)
    this.windows.set(name, win)

    win.on('closed', () => {
      this.windows.delete(name)
    })

    return win
  }

  get(name) {
    return this.windows.get(name)
  }

  closeAll() {
    this.windows.forEach(win => win.close())
  }
}

const windowManager = new WindowManager()

// 使用
windowManager.create('settings', {
  width: 600,
  height: 400
})

窗口类型对比

类型 parent modal frame skipTaskbar 使用场景
主窗口 - - true false 应用主界面
子窗口 false true false 设置、详情页
模态窗口 true true false 确认对话框
无边框窗口 - - false false 自定义UI
托盘窗口 - - false true 快捷操作面板

🔒 安全性

Q6: Electron 应用有哪些常见的安全风险?如何防范?

核心安全风险

1. XSS 攻击(跨站脚本)

风险:

// ❌ 危险:直接插入用户输入
const userInput = '<img src=x onerror="require(\'child_process\').exec(\'rm -rf /\')">'
document.getElementById('content').innerHTML = userInput
// 如果 nodeIntegration: true,会执行系统命令!

防范:

// ✅ 方案 1:禁用 nodeIntegration
webPreferences: {
  nodeIntegration: false,  // ✅ 渲染进程不能使用 Node.js
  contextIsolation: true   // ✅ 隔离上下文
}

// ✅ 方案 2:内容安全策略 (CSP)
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'">

// ✅ 方案 3:转义用户输入
function escapeHTML(str) {
  return str.replace(/[&<>"']/g, (char) => {
    const map = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;'
    }
    return map[char]
  })
}

document.getElementById('content').textContent = userInput  // 使用 textContent

2. Remote Code Execution (RCE)

风险:

// ❌ 极度危险:允许远程代码执行
webPreferences: {
  nodeIntegration: true,
  contextIsolation: false
}

// 渲染进程中
eval(userInput)  // 可以执行任意 Node.js 代码!

防范:

// ✅ 安全配置
const secureWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,       // ✅ 必须
    contextIsolation: true,        // ✅ 必须
    enableRemoteModule: false,     // ✅ 禁用 remote
    sandbox: true,                 // ✅ 沙箱模式
    webSecurity: true,             // ✅ 启用 web 安全
    allowRunningInsecureContent: false,  // ✅ 禁止混合内容
    preload: path.join(__dirname, 'preload.js')
  }
})

// ✅ 通过 contextBridge 暴露安全的 API
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  // 只暴露需要的功能
  readFile: (filename) => ipcRenderer.invoke('read-file', filename)
  // ❌ 不要暴露 eval、require 等危险API
})

3. 不安全的 IPC 通信

风险:

// ❌ 危险:不验证来源
ipcMain.on('delete-file', (event, filepath) => {
  fs.unlinkSync(filepath)  // 恶意网页可以删除任意文件!
})

防范:

// ✅ 验证来源
ipcMain.handle('delete-file', async (event, filepath) => {
  // 1. 验证发送者
  const senderURL = event.senderFrame.url
  if (!senderURL.startsWith('file://')) {
    throw new Error('Unauthorized')
  }

  // 2. 验证路径
  const allowedDir = app.getPath('userData')
  const resolvedPath = path.resolve(filepath)

  if (!resolvedPath.startsWith(allowedDir)) {
    throw new Error('Path not allowed')
  }

  // 3. 检查文件是否存在
  if (!fs.existsSync(resolvedPath)) {
    throw new Error('File not found')
  }

  // 4. 执行操作
  fs.unlinkSync(resolvedPath)
})

// ✅ 使用白名单
const ALLOWED_CHANNELS = [
  'read-file',
  'save-file',
  'get-user-data'
]

ipcMain.handle('secure-invoke', (event, channel, ...args) => {
  if (!ALLOWED_CHANNELS.includes(channel)) {
    throw new Error('Channel not allowed')
  }

  // 执行对应操作
  return handlers[channel](...args)
})

4. 加载不可信内容

风险:

// ❌ 危险:加载外部URL
mainWindow.loadURL('https://evil.com')

// ❌ 危险:允许导航到任意网站
mainWindow.webContents.on('will-navigate', (event, url) => {
  // 没有验证
})

防范:

// ✅ 限制导航
mainWindow.webContents.on('will-navigate', (event, url) => {
  const allowedHosts = [
    'https://myapp.com',
    'https://api.myapp.com'
  ]

  const urlObj = new URL(url)
  if (!allowedHosts.includes(urlObj.origin)) {
    event.preventDefault()  // ✅ 阻止导航
    console.warn('Navigation blocked:', url)
  }
})

// ✅ 禁止新窗口
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
  // 只允许打开特定链接
  if (url.startsWith('https://trusted-site.com')) {
    shell.openExternal(url)  // 在系统浏览器中打开
  }
  return { action: 'deny' }  // 阻止在 Electron 中打开
})

// ✅ 禁用 webview(已废弃)
webPreferences: {
  webviewTag: false  // ✅ 禁用 <webview>
}

5. 中间人攻击 (MITM)

风险:

// ❌ 危险:忽略证书错误
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
  event.preventDefault()
  callback(true)  // 接受所有证书!
})

防范:

// ✅ 验证证书
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
  event.preventDefault()

  // 只在开发环境允许自签名证书
  if (process.env.NODE_ENV === 'development' && url.startsWith('https://localhost')) {
    callback(true)
  } else {
    callback(false)  // ✅ 拒绝不可信证书
    console.error('Certificate error:', error)
  }
})

// ✅ 使用 HTTPS
const secureFetch = (url) => {
  if (!url.startsWith('https://')) {
    throw new Error('Only HTTPS allowed')
  }
  return fetch(url)
}

安全检查清单

// ✅ 完整的安全配置模板
const secureWindowOptions = {
  width: 1200,
  height: 800,
  webPreferences: {
    // 核心安全设置
    nodeIntegration: false,           // ✅ 1. 禁用 Node 集成
    contextIsolation: true,            // ✅ 2. 启用上下文隔离
    enableRemoteModule: false,         // ✅ 3. 禁用 remote
    sandbox: true,                     // ✅ 4. 启用沙箱

    // Web 安全
    webSecurity: true,                 // ✅ 5. Web 安全
    allowRunningInsecureContent: false,// ✅ 6. 禁止混合内容
    experimentalFeatures: false,       // ✅ 7. 禁用实验特性

    // 其他
    webviewTag: false,                 // ✅ 8. 禁用 webview
    navigateOnDragDrop: false,         // ✅ 9. 禁止拖拽导航

    // 预加载脚本
    preload: path.join(__dirname, 'preload.js')
  }
}

// ✅ 安全的 CSP 头
app.on('ready', () => {
  session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
    callback({
      responseHeaders: {
        ...details.responseHeaders,
        'Content-Security-Policy': [
          "default-src 'self'",
          "script-src 'self'",
          "style-src 'self' 'unsafe-inline'",
          "img-src 'self' data: https:",
          "connect-src 'self' https://api.myapp.com"
        ].join('; ')
      }
    })
  })
})

代码审计工具

# 使用 Electronegativity 检查安全问题
npm install -g @doyensec/electronegativity
electronegativity -i /path/to/electron/app

# 输出示例:
# [HIGH] nodeIntegration is enabled
# [MEDIUM] contextIsolation is disabled
# [LOW] enableRemoteModule is not explicitly disabled

安全级别对比

配置 不安全 基本安全 推荐配置
nodeIntegration true ❌ false ✅ false ✅
contextIsolation false ❌ true ✅ true ✅
sandbox false ❌ false ⚠️ true ✅
remote true ❌ false ✅ false ✅
CSP 无 ❌ 基本 ⚠️ 严格 ✅
输入验证 无 ❌ 部分 ⚠️ 完整 ✅

⚡ 性能优化

Q7: Electron 应用启动慢、内存占用高,如何优化?

启动速度优化

1. 延迟加载

// ❌ 启动时加载所有模块
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const lodash = require('lodash')
const moment = require('moment')
// ... 10+ 个模块

app.on('ready', () => {
  createWindow()
})

// ✅ 按需加载
app.on('ready', () => {
  createWindow()
})

// 只在需要时才加载
ipcMain.handle('fetch-data', async () => {
  const axios = require('axios')  // ✅ 用时才加载
  return await axios.get('https://api.example.com/data')
})

2. 使用 V8 快照

// electron-builder.yml
asarUnpack:
  - "**/*.node"
  - "resources/**"

extraResources:
  - from: "v8-snapshot"
    to: "v8-snapshot"

3. 优化窗口创建

//❌ 窗口创建慢(白屏时间长)
const win = new BrowserWindow({ show: true })
win.loadFile('index.html')

// ✅ 先隐藏,加载完再显示
const win = new BrowserWindow({
  show: false,  // ✅ 初始隐藏
  backgroundColor: '#fff'  // ✅ 避免闪烁
})

win.loadFile('index.html')

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

// ✅ 使用启动画面
let splashWindow = new BrowserWindow({
  width: 400,
  height: 300,
  frame: false,
  alwaysOnTop: true
})
splashWindow.loadFile('splash.html')

// 主窗口准备好后关闭启动画面
mainWindow.once('ready-to-show', () => {
  splashWindow.close()
  mainWindow.show()
})

内存优化

1. 避免内存泄漏

// ❌ 内存泄漏示例
class BadWindow {
  constructor() {
    this.data = new Array(1000000).fill('leak')

    ipcMain.on('event', () => {
      // 监听器没有清理
    })

    setInterval(() => {
      // 定时器没有清理
    }, 1000)
  }
}

// ✅ 正确的资源管理
class GoodWindow {
  constructor() {
    this.data = []
    this.cleanup = []

    // 记录需要清理的资源
    const listener = () => {}
    ipcMain.on('event', listener)
    this.cleanup.push(() => ipcMain.off('event', listener))

    const timer = setInterval(() => {}, 1000)
    this.cleanup.push(() => clearInterval(timer))
  }

  destroy() {
    // 清理所有资源
    this.cleanup.forEach(fn => fn())
    this.data = null
  }
}

// 窗口关闭时清理
mainWindow.on('closed', () => {
  goodWindow.destroy()
  mainWindow = null
})

2. 限制并发窗口数

class WindowPool {
  constructor(maxWindows = 5) {
    this.maxWindows = maxWindows
    this.windows = []
  }

  create(options) {
    // 超过限制时复用旧窗口
    if (this.windows.length >= this.maxWindows) {
      const oldWin = this.windows.shift()
      oldWin.close()
    }

    const win = new BrowserWindow(options)
    this.windows.push(win)

    win.on('closed', () => {
      const index = this.windows.indexOf(win)
      if (index > -1) {
        this.windows.splice(index, 1)
      }
    })

    return win
  }
}

3. 减少渲染进程内存

// ✅ 使用虚拟滚动(大列表)
// 渲染进程
import VirtualList from 'react-virtual-list'

function LargeList({ items }) {
  return (
    <VirtualList
      items={items}
      itemHeight={50}
      renderItem={(item) => <div>{item.name}</div>}
    />
  )
}

// ✅ 图片懒加载
<img
  data-src="large-image.jpg"
  loading="lazy"  // 原生懒加载
/>

// ✅ 及时释放大对象
let largeData = fetchLargeData()
processData(largeData)
largeData = null  // ✅ 手动释放

4. 禁用不需要的功能

const win = new BrowserWindow({
  webPreferences: {
    // 禁用 GPU 加速(如果不需要)
    offscreen: false,

    // 禁用拼写检查
    spellcheck: false,

    // 禁用 WebGL(如果不需要)
    webgl: false,

    // 禁用插件
    plugins: false
  }
})

// 禁用硬件加速
app.disableHardwareAcceleration()

渲染性能优化

1. 代码分割

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          minChunks: 2
        }
      }
    }
  }
}

// 路由懒加载
const routes = [
  {
    path: '/settings',
    component: () => import('./views/Settings.vue')
  }
]

2. 使用 Web Workers

// 主线程
const worker = new Worker('worker.js')

worker.postMessage({ data: largeArray })

worker.onmessage = (e) => {
  console.log('处理结果:', e.data)
}

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data.data)
  self.postMessage(result)
}

3. 节流和防抖

import { throttle, debounce } from 'lodash'

// 滚动事件节流
window.addEventListener('scroll', throttle(() => {
  updateScrollPosition()
}, 100))

// 搜索输入防抖
searchInput.addEventListener('input', debounce((e) => {
  performSearch(e.target.value)
}, 300))

打包体积优化

1. Tree Shaking

// ❌ 导入整个库
import _ from 'lodash'
_.get(obj, 'path')

// ✅ 按需导入
import get from 'lodash/get'
get(obj, 'path')

// ✅ 使用 ES Module 版本
import { get } from 'lodash-es'

2. 压缩和混淆

// electron-builder.yml
compression: maximum  # 最大压缩

# package.json
{
  "build": {
    "asar": true,  // ✅ 打包成 asar
    "asarUnpack": [
      "**/*.node"  // 原生模块不打包
    ]
  }
}

3. 移除开发依赖

# 生产环境打包
npm prune --production

# 或使用 electron-builder 自动处理

性能监控

// 主进程性能监控
const { powerMonitor } = require('electron')

app.on('ready', () => {
  // 内存使用
  setInterval(() => {
    const memUsage = process.memoryUsage()
    console.log('主进程内存:', {
      rss: Math.round(memUsage.rss / 1024 / 1024) + ' MB',
      heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + ' MB'
    })
  }, 10000)

  // CPU 使用
  process.cpuUsage()
})

// 渲染进程性能监控
window.addEventListener('load', () => {
  const perfData = performance.getEntriesByType('navigation')[0]
  console.log('页面加载时间:', perfData.loadEventEnd - perfData.fetchStart, 'ms')
})

优化效果对比

优化项 优化前 优化后 提升
启动时间 3.5s 1.2s 66% ↓
内存占用 450 MB 180 MB 60% ↓
包体积 180 MB 65 MB 64% ↓
首屏渲染 2.1s 0.7s 67% ↓

📦 打包部署

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

主流打包工具对比

工具 优势 劣势 推荐度
electron-builder 功能全面、配置灵活、自动更新 配置复杂 ⭐⭐⭐⭐⭐
electron-forge 官方支持、模板丰富 定制性较弱 ⭐⭐⭐⭐
electron-packager 简单快速 功能有限 ⭐⭐⭐

使用 electron-builder(推荐)

1. 安装和基础配置

npm install --save-dev electron-builder
// package.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": "MyApp",

    // 打包文件配置
    "files": [
      "dist/**/*",
      "main.js",
      "preload.js",
      "package.json"
    ],

    // 额外资源
    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/"
      }
    ],

    // Windows 配置
    "win": {
      "target": ["nsis", "portable"],
      "icon": "build/icon.ico",
      "requestedExecutionLevel": "asInvoker"
    },

    // macOS 配置
    "mac": {
      "target": ["dmg", "zip"],
      "icon": "build/icon.icns",
      "category": "public.app-category.productivity",
      "hardenedRuntime": true,
      "gatekeeperAssess": false
    },

    // Linux 配置
    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "build/icons/",
      "category": "Utility"
    },

    // NSIS 安装器配置
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "MyApp"
    }
  }
}

2. 代码签名(Windows)

// package.json
{
  "build": {
    "win": {
      "certificateFile": "path/to/cert.pfx",
      "certificatePassword": "your-password",  // ⚠️ 建议用环境变量
      "signingHashAlgorithms": ["sha256"],
      "sign": "./custom-sign.js"  // 自定义签名脚本
    }
  }
}
// custom-sign.js
exports.default = async function(configuration) {
  const { path } = configuration
  console.log('Signing:', path)

  // 使用 Windows SDK 签名工具
  await require('child_process').execSync(`
    signtool sign /f cert.pfx /p ${process.env.CERT_PASSWORD} /tr http://timestamp.digicert.com /td sha256 /fd sha256 "${path}"
  `)
}

3. 代码签名(macOS)

{
  "build": {
    "mac": {
      "identity": "Developer ID Application: Your Name (TEAM_ID)",
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist"
    }
  }
}
<!-- entitlements.mac.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>
# 公证(Notarization)
export APPLEID="your@email.com"
export APPLEIDPASS="app-specific-password"

electron-builder --mac --publish never
xcrun notarytool submit "dist/MyApp-1.0.0.dmg" \
  --apple-id "$APPLEID" \
  --password "$APPLEIDPASS" \
  --team-id "TEAM_ID"

自动更新

配置自动更新服务器

// main.js
const { app, autoUpdater } = require('electron')
const log = require('electron-log')

const server = 'https://your-update-server.com'
const url = `${server}/update/${process.platform}/${app.getVersion()}`

autoUpdater.setFeedURL({ url })

// 检查更新
autoUpdater.checkForUpdates()

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

autoUpdater.on('update-available', (info) => {
  log.info('有可用更新:', info)
  mainWindow.webContents.send('update-available', info)
})

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

autoUpdater.on('download-progress', (progress) => {
  log.info('下载进度:', progress.percent)
  mainWindow.webContents.send('download-progress', progress)
})

autoUpdater.on('update-downloaded', (info) => {
  log.info('更新下载完成')
  mainWindow.webContents.send('update-downloaded', info)
})

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

// 用户确认后安装更新
ipcMain.on('install-update', () => {
  autoUpdater.quitAndInstall()
})
// 渲染进程
window.electronAPI.onUpdateAvailable((info) => {
  showNotification('发现新版本', `版本 ${info.version} 可用`)
})

window.electronAPI.onDownloadProgress((progress) => {
  updateProgressBar(progress.percent)
})

window.electronAPI.onUpdateDownloaded(() => {
  if (confirm('更新已下载,是否立即重启安装?')) {
    window.electronAPI.installUpdate()
  }
})

发布配置

// package.json
{
  "build": {
    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "your-repo"
      }
    ]
  }
}
# 构建并发布到 GitHub Releases
GH_TOKEN=your_github_token npm run build -- --publish always

# 发布到自建服务器
npm run build -- --publish never
scp dist/*.exe user@server:/var/www/downloads/

多平台打包

# 在 macOS 上打包所有平台
npm run build -- --mac --win --linux

# 使用 Docker 打包 Linux
docker run --rm -ti \
  -v ${PWD}:/project \
  electronuserland/builder:wine \
  /bin/bash -c "npm install && npm run build"

优化打包体积

{
  "build": {
    "asar": true,  // 打包成单文件
    "compression": "maximum",  // 最大压缩

    "files": [
      "!**/*.map",  // 排除 source map
      "!**/*.ts",   // 排除 TypeScript 源文件
      "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}"
    ],

    "asarUnpack": [
      "**/*.node"  // 原生模块不打包
    ]
  }
}

打包结果示例

dist/
├── win-unpacked/           # Windows 未打包版
├── mac/                    # macOS 未打包版
├── MyApp-1.0.0.exe         # Windows 安装器
├── MyApp-1.0.0-portable.exe # Windows 便携版
├── MyApp-1.0.0.dmg         # macOS 镜像
├── MyApp-1.0.0.AppImage    # Linux AppImage
├── MyApp-1.0.0.deb         # Debian 包
└── latest.yml              # 更新清单

💼 实战场景

Q9: 如何实现拖拽上传文件到 Electron 窗口?

// ========== 渲染进程 renderer.js ==========
const dropZone = document.getElementById('drop-zone')

// 阻止默认行为
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  dropZone.addEventListener(eventName, preventDefaults, false)
  document.body.addEventListener(eventName, preventDefaults, false)
})

function preventDefaults(e) {
  e.preventDefault()
  e.stopPropagation()
}

// 视觉反馈
;['dragenter', 'dragover'].forEach(eventName => {
  dropZone.addEventListener(eventName, () => {
    dropZone.classList.add('highlight')
  }, false)
})

;['dragleave', 'drop'].forEach(eventName => {
  dropZone.addEventListener(eventName, () => {
    dropZone.classList.remove('highlight')
  }, false)
})

// 处理文件拖放
dropZone.addEventListener('drop', async (e) => {
  const files = Array.from(e.dataTransfer.files)

  for (const file of files) {
    const result = await window.electronAPI.uploadFile({
      path: file.path,
      name: file.name,
      size: file.size,
      type: file.type
    })

    console.log('上传结果:', result)
  }
}, false)


// ========== 预加载脚本 preload.js ==========
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  uploadFile: (fileInfo) => ipcRenderer.invoke('upload-file', fileInfo)
})


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

ipcMain.handle('upload-file', async (event, fileInfo) => {
  try {
    // 读取文件
    const content = fs.readFileSync(fileInfo.path)

    // 保存到应用数据目录
    const destPath = path.join(app.getPath('userData'), 'uploads', fileInfo.name)
    fs.writeFileSync(destPath, content)

    return { success: true, path: destPath }
  } catch (error) {
    console.error('上传失败:', error)
    throw error
  }
})


// ========== HTML ==========
<div id="drop-zone">
  拖拽文件到这里上传
</div>

<style>
#drop-zone {
  width: 400px;
  height: 200px;
  border: 2px dashed #ccc;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
}

#drop-zone.highlight {
  border-color: #42b983;
  background-color: #f0f9ff;
}
</style>

Q10: 如何实现系统托盘和右键菜单?

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

let tray = null

app.whenReady().then(() => {
  // 创建托盘图标
  const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))

  // Windows 需要调整大小
  if (process.platform === 'win32') {
    icon.resize({ width: 16, height: 16 })
  }

  tray = new Tray(icon)

  // 悬停提示
  tray.setToolTip('我的应用')

  // 创建右键菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示窗口',
      click: () => {
        mainWindow.show()
      }
    },
    {
      label: '刷新',
      accelerator: 'CmdOrCtrl+R',
      click: () => {
        mainWindow.reload()
      }
    },
    {
      type: 'separator'  // 分隔线
    },
    {
      label: '设置',
      submenu: [
        {
          label: '开机启动',
          type: 'checkbox',
          checked: app.getLoginItemSettings().openAtLogin,
          click: (menuItem) => {
            app.setLoginItemSettings({
              openAtLogin: menuItem.checked
            })
          }
        },
        {
          label: '消息通知',
          type: 'checkbox',
          checked: true
        }
      ]
    },
    {
      label: '关于',
      click: () => {
        showAboutDialog()
      }
    },
    {
      type: 'separator'
    },
    {
      label: '退出',
      role: 'quit'
    }
  ])

  // 设置右键菜单
  tray.setContextMenu(contextMenu)

  // 点击托盘图标
  tray.on('click', () => {
    // Windows: 显示/隐藏窗口
    if (mainWindow.isVisible()) {
      mainWindow.hide()
    } else {
      mainWindow.show()
    }
  })

  // 双击托盘图标(macOS)
  tray.on('double-click', () => {
    mainWindow.show()
  })

  // 动态更新菜单
  setInterval(() => {
    const newMenu = Menu.buildFromTemplate([
      {
        label: `当前时间: ${new Date().toLocaleTimeString()}`
      },
      ...contextMenu.items
    ])
    tray.setContextMenu(newMenu)
  }, 1000)
})

// 应用退出时清理
app.on('before-quit', () => {
  if (tray) {
    tray.destroy()
  }
})

📝 总结

这份面试题涵盖了 Electron 开发的核心知识点:

  1. 基础概念 - 理解 Electron 的架构和双进程模型
  2. 进程通信 - 掌握 IPC 的各种通信方式
  3. 窗口管理 - 熟悉不同类型窗口的创建和管理
  4. 安全性 - 了解常见安全风险和防范措施
  5. 性能优化 - 掌握启动速度、内存、打包体积的优化技巧
  6. 打包部署 - 熟悉应用的打包、签名和自动更新
  7. 实战场景 - 能够解决实际开发中的常见问题

建议按顺序学习,从基础到进阶逐步深入,并结合实际项目练习。祝你面试顺利!🚀