定义:
Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的框架,由 GitHub 开发。
核心组成:
Electron = Chromium + Node.js + Native APIs
↓ ↓ ↓
渲染引擎 后端能力 系统调用
主要优势:
// 一套代码,三个平台
const app = {
platforms: ['Windows', 'macOS', 'Linux'],
code: 'Write once, run everywhere'
}
// 前端开发者可以直接上手
import React from 'react'
import { ipcRenderer } from 'electron'
function App() {
const handleClick = () => {
ipcRenderer.send('native-action')
}
return <button onClick={handleClick}>调用系统 API</button>
}
# 可以使用所有 npm 包
npm install axios lodash electron-store
// 访问文件系统
const fs = require('fs')
const path = require('path')
// 调用系统通知
const { Notification } = require('electron')
new Notification({ title: '提示', body: '操作完成' }).show()
劣势:
典型应用:
双进程架构:
┌─────────────────────────────────────────┐
│ 主进程 (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 渲染、用户交互 |
核心方式:IPC (Inter-Process Communication)
// ========== 预加载脚本 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)
})
// ========== 预加载脚本 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
}
})
// ========== 主进程 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)
})
// ========== 主进程 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)
})
// ========== 主进程 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')
})
// ❌ 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)
核心区别:
| 特性 | 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('用户不存在') // ✅ 异常自动传递
}
})
// ❌ 老旧写法,代码复杂
// ========== 渲染进程 ==========
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))
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
})
})
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()
})
子窗口特性:
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()
})
})
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()
})
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()
})
}
})
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 | 快捷操作面板 |
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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
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 | 无 ❌ | 基本 ⚠️ | 严格 ✅ |
| 输入验证 | 无 ❌ | 部分 ⚠️ | 完整 ✅ |
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% ↓ |
| 工具 | 优势 | 劣势 | 推荐度 |
|---|---|---|---|
| electron-builder | 功能全面、配置灵活、自动更新 | 配置复杂 | ⭐⭐⭐⭐⭐ |
| electron-forge | 官方支持、模板丰富 | 定制性较弱 | ⭐⭐⭐⭐ |
| electron-packager | 简单快速 | 功能有限 | ⭐⭐⭐ |
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 # 更新清单
// ========== 渲染进程 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>
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 开发的核心知识点:
建议按顺序学习,从基础到进阶逐步深入,并结合实际项目练习。祝你面试顺利!🚀