Electron桌面开发

What — 是什么

Electron 是 GitHub 推出的桌面应用开发框架,将 Chromium 和 Node.js 合并到同一个运行时中,让开发者用 Web 技术(HTML/CSS/JS)构建跨平台桌面应用。VS Code、Discord、Slack、Figma 等知名应用均基于 Electron。

核心概念:

  • 主进程(Main Process):运行 Node.js,管理窗口和系统交互,每个应用只有一个
  • 渲染进程(Renderer Process):运行 Chromium,负责页面渲染,可有多个
  • IPC 通信:主进程与渲染进程通过 ipcMain / ipcRenderer 通信
  • Preload 脚本:在渲染进程加载前运行,安全地桥接主进程和渲染进程
  • BrowserWindow:窗口管理类,创建和控制应用窗口
  • Tray:系统托盘,常驻后台运行

核心架构:

┌─────────────────────────────────────────────┐
│                  Electron App                │
├──────────────────┬──────────────────────────┤
│   Main Process   │   Renderer Process(es)   │
│   (Node.js)      │   (Chromium)             │
│                  │                          │
│  - BrowserWindow │  - Vue/React UI          │
│  - IPC Handler   │  - User Interaction      │
│  - System API    │  - DOM Rendering         │
│  - File System   │                          │
│  - Native Module │  Preload Bridge          │
│  - Tray/Menu     │  - contextBridge         │
│                  │  - ipcRenderer.invoke    │
├──────────────────┴──────────────────────────┤
│            IPC (Inter-Process Communication) │
└─────────────────────────────────────────────┘
  • 设计理念:Web 技术 + Node.js 能力 = 桌面应用
  • 核心模块:BrowserWindow、ipcMain/ipcRenderer、BrowserView、Tray、Menu、net、shell
  • 数据流:用户操作 → 渲染进程 UI 事件 → IPC → 主进程 Node.js 处理 → IPC → 渲染进程更新

进程模型:

进程运行环境数量职责
主进程Node.js1窗口管理、系统交互、原生 API
渲染进程ChromiumN页面渲染、用户交互
GPU 进程Chromium1GPU 加速渲染
PreloadNode.js(受限)N/窗口安全桥接
UtilityNode.jsN网络请求等辅助任务

关键特性:

  • 跨平台:一套代码编译 Windows / macOS / Linux
  • 完整 Node.js 能力:文件系统、原生模块、系统调用、子进程
  • Chromium 渲染:CSS3、WebGL、DevTools 全部支持
  • 自动更新:electron-updater 支持 GitHub/自定义服务器
  • 原生集成:系统托盘、通知、菜单、快捷键、剪贴板

Why — 为什么

适用场景:

  • 需要 Node.js 原生能力的桌面应用(文件操作、系统调用)
  • Web 应用需要离线运行或系统集成
  • 开发工具类应用(VS Code、Figma 都基于 Electron)
  • 内部工具,团队以 Web 技术栈为主
  • 需要跨平台但不想维护三套原生代码

知名应用案例:

应用说明特色
VS Code微软代码编辑器插件生态、性能优化标杆
Discord语音通讯原生集成、自动更新
Slack企业协作多窗口、系统托盘
Figma设计工具WebGL 渲染、高性能
Notion笔记工具离线缓存、跨平台
Obsidian知识管理本地文件、插件系统

对比同类框架:

维度ElectronTauriNW.jsFlutter DesktopQt
语言JS/TSRust + Web前端JSDartC++
底层Chromium + Node系统 WebViewChromium + NodeSkia自绘
包体积大(~100MB+)小(~5MB)
内存占用
原生能力Node.jsRust FFINode.jsFFI原生
学习曲线
生态最成熟快速增长一般增长中成熟
WebView 一致性极好(自带 Chromium)依赖系统极好N/AN/A

优缺点:

  • ✅ 优点:
    • Web 技术栈直接复用,前端团队零门槛
    • 生态最成熟,社区资源丰富
    • Node.js 完整能力,原生模块支持好
    • DevTools 调试方便,开发体验好
    • 知名案例多,最佳实践丰富
    • Chromium 渲染一致性,无兼容性问题
  • ❌ 缺点:
    • 包体积大,Chromium 内核打包进去
    • 内存占用高,多进程架构开销大
    • 性能不如原生,CPU 密集型任务吃力
    • 安全风险,需严格限制渲染进程权限
    • 启动速度较慢

How — 怎么用

快速上手

# 创建项目(推荐 electron-vite)
npm create @quick-start/electron my-app -- --template vue-ts
cd my-app
npm install
npm run dev

# 或使用 Electron Forge
npm init electron-app@latest my-app -- --template=vite-typescript

# 或手动搭建
mkdir my-app && cd my-app
npm init -y
npm install electron --save-dev
npm install electron-builder --save-dev

项目结构:

my-app/
├── src/
│   ├── main/              # 主进程代码
│   │   ├── index.ts       # 入口
│   │   ├── window.ts      # 窗口管理
│   │   ├── ipc.ts         # IPC 注册
│   │   ├── tray.ts        # 托盘
│   │   └── menu.ts        # 菜单
│   ├── preload/           # Preload 脚本
│   │   └── index.ts
│   └── renderer/          # 渲染进程(Vue/React)
│       └── src/
├── resources/             # 静态资源(图标、安装包素材)
│   ├── icon.ico           # Windows 图标
│   ├── icon.icns          # macOS 图标
│   └── tray.png           # 托盘图标
├── electron.vite.config.ts
├── electron-builder.yml   # 打包配置
└── package.json

主进程 — 窗口管理

创建窗口:

// src/main/window.ts
import { app, BrowserWindow, screen } from 'electron';
import { join } from 'path';
import isDev from 'electron-is-dev';

interface WindowOptions {
  width?: number;
  height?: number;
  url?: string;
  file?: string;
}

const windows = new Map<string, BrowserWindow>();

export function createWindow(name: string, options: WindowOptions = {}) {
  const { width = 1200, height = 800 } = options;

  // 获取窗口上次位置
  const bounds = loadWindowBounds(name);
  const win = new BrowserWindow({
    width: bounds?.width || width,
    height: bounds?.height || height,
    x: bounds?.x,
    y: bounds?.y,
    minWidth: 800,
    minHeight: 600,
    frame: false,              // 无边框窗口
    titleBarStyle: 'hidden',   // macOS 隐藏标题栏(保留交通灯按钮)
    backgroundColor: '#1a1a1a', // 防止白屏闪烁
    show: false,               // 先隐藏,ready-to-show 后再显示
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: true,
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  // 窗口准备好再显示,避免白屏
  win.once('ready-to-show', () => {
    win.show();
  });

  // 记住窗口位置
  win.on('close', () => {
    saveWindowBounds(name, win.getBounds());
    windows.delete(name);
  });

  // 加载内容
  if (isDev && process.env.ELECTRON_RENDERER_URL) {
    win.loadURL(process.env.ELECTRON_RENDERER_URL);
    win.webContents.openDevTools();
  } else {
    win.loadFile(join(__dirname, `../renderer/${options.file || 'index'}.html`));
  }

  windows.set(name, win);
  return win;
}

// 窗口位置持久化
function loadWindowBounds(name: string) {
  try {
    return JSON.parse(localStorage.getItem(`window-bounds-${name}`) || 'null');
  } catch { return null; }
}

function saveWindowBounds(name: string, bounds: Electron.Rectangle) {
  localStorage.setItem(`window-bounds-${name}`, JSON.stringify(bounds));
}

export function getWindow(name: string) {
  return windows.get(name);
}

多窗口管理:

// src/main/index.ts
import { app } from 'electron';
import { createWindow } from './window';

app.whenReady().then(() => {
  createWindow('main', { width: 1200, height: 800 });
});

// macOS 点击 dock 图标时重新创建窗口
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow('main');
  }
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

父子窗口与模态窗口:

// 模态窗口(阻塞父窗口)
const parent = getWindow('main');
const modal = new BrowserWindow({
  width: 500,
  height: 400,
  parent,                    // 指定父窗口
  modal: true,               // 模态窗口
  show: false,
});

modal.once('ready-to-show', () => modal.show());
modal.loadFile(join(__dirname, '../renderer/modal.html'));

// 子窗口跟随父窗口
parent.on('move', () => {
  const [x, y] = parent.getPosition();
  child.setPosition(x + 20, y + 20);
});

BrowserView — 嵌入网页:

// 在主窗口中嵌入另一个网页(如帮助文档)
import { BrowserView } from 'electron';

const view = new BrowserView({
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
  }
});

mainWindow.setBrowserView(view);
view.setBounds({ x: 0, y: 40, width: 1200, height: 760 });
view.webContents.loadURL('https://docs.example.com');

// 移除
mainWindow.setBrowserView(null);
view.webContents.destroy();

Preload — 安全桥接

// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';

// 通过 contextBridge 暴露安全 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // === 文件操作 ===
  selectFile: () => ipcRenderer.invoke('dialog:selectFile'),
  selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'),
  readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
  writeFile: (path: string, content: string) =>
    ipcRenderer.invoke('fs:writeFile', path, content),

  // === 系统信息 ===
  getPlatform: () => process.platform,
  getAppVersion: () => ipcRenderer.invoke('app:getVersion'),
  getSystemInfo: () => ipcRenderer.invoke('system:getInfo'),

  // === 窗口控制 ===
  minimize: () => ipcRenderer.invoke('window:minimize'),
  maximize: () => ipcRenderer.invoke('window:maximize'),
  close: () => ipcRenderer.invoke('window:close'),
  isMaximized: () => ipcRenderer.invoke('window:isMaximized'),

  // === 事件监听(主进程 → 渲染进程) ===
  onUpdateAvailable: (callback: (info: any) => void) =>
    ipcRenderer.on('update:available', (_e, info) => callback(info)),
  onUpdateDownloaded: (callback: () => void) =>
    ipcRenderer.on('update:downloaded', () => callback()),
  onDeepLink: (callback: (url: string) => void) =>
    ipcRenderer.on('deep-link', (_e, url) => callback(url)),

  // === 清理监听 ===
  removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel),
});

TypeScript 类型声明:

// src/preload/index.d.ts
export interface ElectronAPI {
  selectFile: () => Promise<string | null>;
  selectFolder: () => Promise<string | null>;
  readFile: (path: string) => Promise<string>;
  writeFile: (path: string, content: string) => Promise<void>;
  getPlatform: () => string;
  getAppVersion: () => Promise<string>;
  getSystemInfo: () => Promise<SystemInfo>;
  minimize: () => Promise<void>;
  maximize: () => Promise<void>;
  close: () => Promise<void>;
  isMaximized: () => Promise<boolean>;
  onUpdateAvailable: (callback: (info: any) => void) => void;
  onUpdateDownloaded: (callback: () => void) => void;
  onDeepLink: (callback: (url: string) => void) => void;
  removeAllListeners: (channel: string) => void;
}

declare global {
  interface Window {
    electronAPI: ElectronAPI;
  }
}

IPC 通信详解

三种通信方式对比:

方式方向返回值使用场景
invoke/handle双向Promise请求-响应模式(最常用)
send/on单向事件通知
postMessage双向跨上下文通信

双向通信 — invoke/handle(推荐):

// 主进程 — 注册 handler
ipcMain.handle('dialog:selectFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Images', extensions: ['png', 'jpg', 'gif'] },
      { name: 'Documents', extensions: ['pdf', 'doc', 'docx'] },
      { name: 'All Files', extensions: ['*'] },
    ],
  });
  return canceled ? null : filePaths[0];
});

ipcMain.handle('dialog:selectFolder', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openDirectory'],
  });
  return canceled ? null : filePaths[0];
});

ipcMain.handle('fs:readFile', async (_e, filePath: string) => {
  try {
    const buffer = await fs.promises.readFile(filePath);
    return buffer.toString('base64');
  } catch (err) {
    throw new Error(`读取文件失败: ${err.message}`);
  }
});

ipcMain.handle('fs:writeFile', async (_e, filePath: string, content: string) => {
  await fs.promises.writeFile(filePath, content, 'utf-8');
});
// 渲染进程 — 调用
const filePath = await window.electronAPI.selectFile();
if (filePath) {
  const base64 = await window.electronAPI.readFile(filePath);
}

单向通信 — send/on:

// 渲染进程 → 主进程(不需要返回值)
// 在 preload 中
contextBridge.exposeInMainWorld('electronAPI', {
  notify: (data: any) => ipcRenderer.send('notification:show', data),
});

// 主进程监听
ipcMain.on('notification:show', (_e, data) => {
  new Notification({ title: data.title, body: data.body }).show();
});

// 主进程 → 渲染进程
mainWindow.webContents.send('update:available', { version: '1.2.0' });

MessagePort — 双向流式通信:

// 主进程 — 创建 MessageChannel
ipcMain.handle('chat:createChannel', (event) => {
  const { port1, port2 } = new MessageChannelMain();
  // port1 给主进程用
  port1.on('message', (e) => {
    console.log('渲染进程消息:', e.data);
    port1.postMessage({ reply: '收到' });
  });
  port1.start();
  // port2 发给渲染进程
  event.senderFrame.postMessage('chat:port', null, [port2]);
  return true;
});

安全注意事项:

// ❌ 危险:暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('ipc', ipcRenderer);

// ✅ 安全:只暴露特定方法
contextBridge.exposeInMainWorld('electronAPI', {
  selectFile: () => ipcRenderer.invoke('dialog:selectFile'),
});

// ❌ 危险:直接在 handler 中信任渲染进程传来的路径
ipcMain.handle('fs:readFile', async (_e, filePath: string) => {
  return fs.readFileSync(filePath); // 路径遍历攻击!
});

// ✅ 安全:验证路径
ipcMain.handle('fs:readFile', async (_e, filePath: string) => {
  const resolved = path.resolve(filePath);
  if (!resolved.startsWith(APP_DATA_DIR)) {
    throw new Error('路径越权');
  }
  return fs.readFileSync(resolved);
});

数据持久化

electron-store — 轻量 JSON 存储:

// src/main/store.ts
import Store from 'electron-store';

interface StoreSchema {
  windowBounds: Record<string, Electron.Rectangle>;
  recentFiles: string[];
  settings: {
    theme: 'light' | 'dark' | 'system';
    language: string;
    autoUpdate: boolean;
    minimizeToTray: boolean;
  };
}

const store = new Store<StoreSchema>({
  defaults: {
    windowBounds: {},
    recentFiles: [],
    settings: {
      theme: 'system',
      language: 'zh-CN',
      autoUpdate: true,
      minimizeToTray: false,
    },
  },
  encryptionKey: 'your-encryption-key', // 加密敏感数据
});

export default store;

// 使用
store.set('settings.theme', 'dark');
store.get('settings.theme'); // 'dark'
store.delete('recentFiles');
store.has('settings'); // true

better-sqlite3 — SQLite 数据库:

// src/main/database.ts
import Database from 'better-sqlite3';
import { app } from 'electron';
import { join } from 'path';

const dbPath = join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);

// 启用 WAL 模式(提升并发性能)
db.pragma('journal_mode = WAL');

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT DEFAULT '',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );

  CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updated_at);
`);

// 预编译语句(性能优化)
const insertNote = db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)');
const getNote = db.prepare('SELECT * FROM notes WHERE id = ?');
const listNotes = db.prepare('SELECT * FROM notes ORDER BY updated_at DESC LIMIT ? OFFSET ?');
const updateNote = db.prepare('UPDATE notes SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
const deleteNote = db.prepare('DELETE FROM notes WHERE id = ?');

// 事务
const insertMany = db.transaction((notes: { title: string; content: string }[]) => {
  for (const note of notes) {
    insertNote.run(note.title, note.content);
  }
});

// IPC 注册
ipcMain.handle('db:insertNote', (_e, title: string, content: string) => {
  const result = insertNote.run(title, content);
  return result.lastInsertRowid;
});

ipcMain.handle('db:getNote', (_e, id: number) => {
  return getNote.get(id);
});

ipcMain.handle('db:listNotes', (_e, limit: number, offset: number) => {
  return listNotes.all(limit, offset);
});

ipcMain.handle('db:updateNote', (_e, id: number, title: string, content: string) => {
  return updateNote.run(title, content, id);
});

ipcMain.handle('db:deleteNote', (_e, id: number) => {
  return deleteNote.run(id);
});

// 应用退出时关闭
app.on('before-quit', () => db.close());

文件存储 — 大文件/二进制:

// src/main/fileStorage.ts
import { app } from 'electron';
import { join } from 'path';
import fs from 'fs/promises';

const DATA_DIR = join(app.getPath('userData'), 'data');

export async function saveBuffer(filename: string, buffer: Buffer) {
  await fs.mkdir(DATA_DIR, { recursive: true });
  const filePath = join(DATA_DIR, filename);
  await fs.writeFile(filePath, buffer);
  return filePath;
}

export async function readBuffer(filename: string) {
  return fs.readFile(join(DATA_DIR, filename));
}

export async function deleteFile(filename: string) {
  return fs.unlink(join(DATA_DIR, filename));
}

export async function listFiles() {
  await fs.mkdir(DATA_DIR, { recursive: true });
  return fs.readdir(DATA_DIR);
}

原生菜单与快捷键

应用菜单:

// src/main/menu.ts
import { Menu, shell, app, BrowserWindow } from 'electron';

const isMac = process.platform === 'darwin';

export function createAppMenu() {
  const template: Electron.MenuItemConstructorOptions[] = [
    // macOS 应用菜单
    ...(isMac ? [{
      label: app.name,
      submenu: [
        { label: `关于 ${app.name}`, role: 'about' },
        { type: 'separator' },
        { label: '偏好设置', accelerator: 'CmdOrCtrl+,', click: openSettings },
        { type: 'separator' },
        { label: '隐藏', role: 'hide' },
        { label: '隐藏其他', role: 'hideOthers' },
        { label: '显示全部', role: 'unhide' },
        { type: 'separator' },
        { label: '退出', accelerator: 'CmdOrCtrl+Q', role: 'quit' },
      ],
    }] : []),
    {
      label: '文件',
      submenu: [
        { label: '新建', accelerator: 'CmdOrCtrl+N', click: handleNew },
        { label: '打开', accelerator: 'CmdOrCtrl+O', click: handleOpen },
        { label: '保存', accelerator: 'CmdOrCtrl+S', click: handleSave },
        { label: '另存为', accelerator: 'CmdOrCtrl+Shift+S', click: handleSaveAs },
        { type: 'separator' },
        { label: '最近打开', role: 'recentDocuments' },
        { type: 'separator' },
        isMac ? { label: '关闭窗口', role: 'close' } : { label: '退出', role: 'quit' },
      ],
    },
    {
      label: '编辑',
      submenu: [
        { label: '撤销', role: 'undo' },
        { label: '重做', role: 'redo' },
        { type: 'separator' },
        { label: '剪切', role: 'cut' },
        { label: '复制', role: 'copy' },
        { label: '粘贴', role: 'paste' },
        { label: '全选', role: 'selectAll' },
      ],
    },
    {
      label: '视图',
      submenu: [
        { label: '重新加载', role: 'reload' },
        { label: '强制重新加载', role: 'forceReload' },
        { label: '开发者工具', role: 'toggleDevTools' },
        { type: 'separator' },
        { label: '放大', role: 'zoomIn' },
        { label: '缩小', role: 'zoomOut' },
        { label: '重置缩放', role: 'resetZoom' },
        { type: 'separator' },
        { label: '全屏', role: 'togglefullscreen' },
      ],
    },
    {
      label: '帮助',
      submenu: [
        { label: '文档', click: () => shell.openExternal('https://docs.example.com') },
        { label: '检查更新', click: checkForUpdates },
        { type: 'separator' },
        { label: '关于', click: showAboutDialog },
      ],
    },
  ];

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

右键上下文菜单:

import { Menu } from 'electron';

// 主进程监听右键菜单请求
ipcMain.handle('context-menu:show', (event, options) => {
  const menu = Menu.buildFromTemplate([
    { label: '复制', role: 'copy' },
    { label: '粘贴', role: 'paste' },
    { type: 'separator' },
    { label: '剪切', role: 'cut' },
    { type: 'separator' },
    {
      label: '另存为...',
      click: () => {
        BrowserWindow.fromWebContents(event.sender)?.webContents.send('context-menu:save-as');
      }
    },
  ]);

  menu.popup({ window: BrowserWindow.fromWebContents(event.sender)! });
});

全局快捷键:

import { globalShortcut, app } from 'electron';

app.whenReady().then(() => {
  // 注册全局快捷键(应用未聚焦时也生效)
  const ret = globalShortcut.register('CommandOrControl+Shift+V', () => {
    mainWindow.show();
    mainWindow.webContents.send('shortcut:paste-special');
  });

  if (!ret) console.error('快捷键注册失败');

  // 检查是否注册成功
  console.log(globalShortcut.isRegistered('CommandOrControl+Shift+V'));
});

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

系统托盘

// src/main/tray.ts
import { Tray, Menu, nativeImage, app, BrowserWindow } from 'electron';
import { join } from 'path';
import store from './store';

let tray: Tray | null = null;

export function createTray(mainWindow: BrowserWindow) {
  const iconPath = join(__dirname, '../../resources/tray.png');
  const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });

  tray = new Tray(icon);
  tray.setToolTip('My App');

  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', click: () => {
      mainWindow.show();
      mainWindow.focus();
    }},
    { type: 'separator' },
    { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => {
      mainWindow.show();
      mainWindow.webContents.send('action:new');
    }},
    { type: 'separator' },
    { label: '偏好设置', click: () => {
      mainWindow.show();
      mainWindow.webContents.send('navigate:settings');
    }},
    { type: 'separator' },
    { label: '退出', click: () => app.quit() },
  ]);

  tray.setContextMenu(contextMenu);

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

  // 最小化到托盘
  mainWindow.on('close', (event) => {
    if (store.get('settings.minimizeToTray')) {
      event.preventDefault();
      mainWindow.hide();
    }
  });
}

系统对话框与剪贴板

import { dialog, clipboard, nativeImage, shell } from 'electron';
import fs from 'fs/promises';

// === 文件对话框 ===

// 打开文件
ipcMain.handle('dialog:selectFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    title: '选择文件',
    properties: ['openFile', 'multiSelections'],
    filters: [
      { name: '图片', extensions: ['png', 'jpg', 'gif', 'webp'] },
      { name: '文档', extensions: ['pdf', 'doc', 'docx', 'txt'] },
      { name: '所有文件', extensions: ['*'] },
    ],
  });
  return canceled ? null : filePaths;
});

// 保存文件
ipcMain.handle('dialog:saveFile', async (_e, defaultName: string, content: string) => {
  const { canceled, filePath } = await dialog.showSaveDialog({
    title: '保存文件',
    defaultPath: defaultName,
    filters: [{ name: 'Text', extensions: ['txt'] }],
  });
  if (canceled || !filePath) return null;
  await fs.writeFile(filePath, content, 'utf-8');
  return filePath;
});

// 错误对话框
ipcMain.handle('dialog:showError', (_e, title: string, message: string) => {
  dialog.showErrorBox(title, message);
});

// === 剪贴板 ===

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

// 写入剪贴板文本
ipcMain.handle('clipboard:writeText', (_e, text: string) => {
  clipboard.writeText(text);
});

// 读取剪贴板图片
ipcMain.handle('clipboard:readImage', () => {
  const image = clipboard.readImage();
  return image.isEmpty() ? null : image.toDataURL();
});

// 写入剪贴板图片
ipcMain.handle('clipboard:writeImage', (_e, dataUrl: string) => {
  const image = nativeImage.createFromDataURL(dataUrl);
  clipboard.writeImage(image);
});

// === shell ===

// 在默认浏览器打开链接
ipcMain.handle('shell:openExternal', (_e, url: string) => {
  // 安全检查:只允许 http/https 协议
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    throw new Error('不允许的协议');
  }
  shell.openExternal(url);
});

// 在文件管理器中显示
ipcMain.handle('shell:showItemInFolder', (_e, fullPath: string) => {
  shell.showItemInFolder(fullPath);
});

// 打开文件
ipcMain.handle('shell:openPath', async (_e, path: string) => {
  return shell.openPath(path);
});

无边框窗口与自定义标题栏

主进程窗口控制:

ipcMain.handle('window:minimize', (e) => {
  BrowserWindow.fromWebContents(e.sender)?.minimize();
});

ipcMain.handle('window:maximize', (e) => {
  const win = BrowserWindow.fromWebContents(e.sender);
  if (!win) return;
  win.isMaximized() ? win.unmaximize() : win.maximize();
});

ipcMain.handle('window:close', (e) => {
  BrowserWindow.fromWebContents(e.sender)?.close();
});

ipcMain.handle('window:isMaximized', (e) => {
  return BrowserWindow.fromWebContents(e.sender)?.isMaximized();
});

// 监听最大化状态变化
mainWindow.on('maximize', () => {
  mainWindow.webContents.send('window:maximized-changed', true);
});
mainWindow.on('unmaximize', () => {
  mainWindow.webContents.send('window:maximized-changed', false);
});

渲染进程 — 自定义标题栏组件:

<!-- TitleBar.vue -->
<template>
  <div class="title-bar">
    <div class="drag-area">
      <img src="/logo.png" class="app-icon" />
      <span class="app-title">My App</span>
    </div>
    <div class="window-controls">
      <button class="control-btn" @click="minimize" title="最小化">
        <svg width="12" height="12"><line x1="0" y1="6" x2="12" y2="6" /></svg>
      </button>
      <button class="control-btn" @click="toggleMaximize" title="最大化">
        <svg v-if="!isMaximized" width="12" height="12"><rect x="1" y="1" width="10" height="10" /></svg>
        <svg v-else width="12" height="12"><rect x="3" y="0" width="9" height="9" /><rect x="0" y="3" width="9" height="9" /></svg>
      </button>
      <button class="control-btn close" @click="close" title="关闭">
        <svg width="12" height="12"><line x1="0" y1="0" x2="12" y2="12" /><line x1="12" y1="0" x2="0" y2="12" /></svg>
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const isMaximized = ref(false);

const minimize = () => window.electronAPI.minimize();
const toggleMaximize = () => window.electronAPI.maximize();
const close = () => window.electronAPI.close();

onMounted(async () => {
  isMaximized.value = await window.electronAPI.isMaximized();
  window.electronAPI.onMaximizedChanged?.((maximized: boolean) => {
    isMaximized.value = maximized;
  });
});
</script>

<style scoped>
.title-bar {
  -webkit-app-region: drag;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #1e1e1e;
  user-select: none;
}

.drag-area {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-left: 12px;
}

.app-icon { width: 16px; height: 16px; }
.app-title { font-size: 13px; color: #ccc; }

.window-controls {
  -webkit-app-region: no-drag;
  display: flex;
  height: 100%;
}

.control-btn {
  width: 46px;
  height: 100%;
  border: none;
  background: transparent;
  color: #ccc;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.control-btn:hover { background: rgba(255,255,255,0.1); }
.control-btn.close:hover { background: #e81123; color: white; }
</style>

自动更新

// src/main/updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';

export function setupAutoUpdater(mainWindow: BrowserWindow) {
  autoUpdater.autoDownload = false;
  autoUpdater.autoInstallOnAppQuit = true;

  // 配置更新源
  autoUpdater.setFeedURL({
    provider: 'github',
    owner: 'my-org',
    repo: 'my-app',
  });

  // 检查更新(启动时 + 定时)
  app.whenReady().then(() => {
    autoUpdater.checkForUpdates();
    // 每 4 小时检查一次
    setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
  });

  autoUpdater.on('checking-for-update', () => {
    mainWindow.webContents.send('update:checking');
  });

  autoUpdater.on('update-available', (info) => {
    mainWindow.webContents.send('update:available', {
      version: info.version,
      releaseNotes: info.releaseNotes,
    });
  });

  autoUpdater.on('update-not-available', () => {
    mainWindow.webContents.send('update:not-available');
  });

  autoUpdater.on('error', (err) => {
    mainWindow.webContents.send('update:error', err.message);
  });

  autoUpdater.on('download-progress', (progress) => {
    mainWindow.webContents.send('update:progress', {
      percent: progress.percent,
      transferred: progress.transferred,
      total: progress.total,
    });
  });

  autoUpdater.on('update-downloaded', () => {
    mainWindow.webContents.send('update:downloaded');
  });

  // IPC — 手动检查更新
  ipcMain.handle('update:check', () => autoUpdater.checkForUpdates());

  // IPC — 下载更新
  ipcMain.handle('update:download', () => autoUpdater.downloadUpdate());

  // IPC — 安装更新并重启
  ipcMain.handle('update:install', () => {
    autoUpdater.quitAndInstall(false, true);
  });
}

渲染进程更新 UI:

<!-- UpdateNotification.vue -->
<template>
  <Transition name="slide">
    <div v-if="updateInfo" class="update-banner">
      <span>新版本 v{{ updateInfo.version }} 可用</span>
      <button v-if="!downloading" @click="downloadUpdate">立即更新</button>
      <div v-else class="progress-bar">
        <div class="progress-fill" :style="{ width: progress + '%' }" />
      </div>
      <button v-if="readyToInstall" @click="installUpdate">重启安装</button>
      <button class="dismiss" @click="dismiss">稍后</button>
    </div>
  </Transition>
</template>

安全最佳实践

安全配置清单:

// src/main/security.ts
import { app, session } from 'electron';

app.whenReady().then(() => {
  // 1. 设置 CSP(内容安全策略)
  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.example.com; " +
          "font-src 'self';"
        ],
      },
    });
  });

  // 2. 禁止导航到外部网站
  session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
    const url = new URL(details.url);
    if (url.protocol === 'http:' || url.protocol === 'https:') {
      if (!url.hostname.endsWith('example.com')) {
        // 在外部浏览器打开
        shell.openExternal(details.url);
        callback({ cancel: true });
        return;
      }
    }
    callback({});
  });

  // 3. 禁止新窗口打开
  mainWindow.webContents.setWindowOpenHandler(() => {
    return { action: 'deny' };
  });

  // 4. 禁用 remote 模块
  // 不安装 @electron/remote(已废弃)
});

// 5. 验证 IPC 来源
ipcMain.handle('sensitive:action', (event) => {
  // 验证请求来自受信任的渲染进程
  const frameUrl = event.senderFrame.url;
  if (!frameUrl.startsWith('file://') && !frameUrl.includes('localhost')) {
    throw new Error('未授权的请求来源');
  }
  // 执行敏感操作
});

webPreferences 安全配置:

const secureWebPreferences: Electron.WebPreferences = {
  nodeIntegration: false,       // ❗ 必须关闭
  contextIsolation: true,       // ❗ 必须开启
  sandbox: true,                // ❗ 推荐:沙箱隔离
  webSecurity: true,            // 同源策略
  allowRunningInsecureContent: false,
  enableRemoteModule: false,    // 禁用 remote
  preload: join(__dirname, '../preload/index.js'),
};

打包与分发

electron-builder 配置:

# electron-builder.yml
appId: com.example.myapp
productName: My App
copyright: Copyright © 2026

directories:
  output: dist
  buildResources: resources

files:
  - src/main/**/*
  - src/preload/**/*
  - src/renderer/dist/**/*
  - package.json
  - "!**/*.map"

# Windows
win:
  target:
    - target: nsis
      arch: [x64]
  icon: resources/icon.ico
  artifactName: ${name}-${version}-setup.${ext}

nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true
  installerIcon: resources/icon.ico
  uninstallerIcon: resources/icon.ico
  installerHeaderIcon: resources/icon.ico
  createDesktopShortcut: true
  createStartMenuShortcut: true
  shortcutName: My App

# macOS
mac:
  target:
    - target: dmg
      arch: [x64, arm64]
  icon: resources/icon.icns
  category: public.app-category.productivity
  hardenedRuntime: true
  gatekeeperAssess: false
  entitlements: resources/entitlements.mac.plist
  entitlementsInherit: resources/entitlements.mac.plist

dmg:
  title: ${name} ${version}
  artifactName: ${name}-${version}-${arch}.${ext}

# Linux
linux:
  target:
    - target: AppImage
      arch: [x64]
    - target: deb
      arch: [x64]
  icon: resources
  category: Development

# 自动更新
publish:
  provider: github
  owner: my-org
  repo: my-app

打包命令:

# 打包当前平台
npm run build

# 打包指定平台
npx electron-builder --win
npx electron-builder --mac
npx electron-builder --linux

# 打包所有平台
npx electron-builder --win --mac --linux

# 只生成不打包(调试)
npx electron-builder --dir

代码签名(macOS):

# 设置环境变量
export CSC_LINK=/path/to/certificate.p12
export CSC_KEY_PASSWORD=your-password
export APPLE_ID=your@email.com
export APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
export APPLE_TEAM_ID=XXXXXXXXXX

# electron-builder 会自动签名和公证
npx electron-builder --mac

electron-forge 配置(替代方案):

// forge.config.ts
import { VitePlugin } from '@electron-forge/plugin-vite';

export default {
  packagerConfig: {
    name: 'My App',
    appBundleId: 'com.example.myapp',
    icon: 'resources/icon',
    asar: true,
  },
  makers: [
    { name: '@electron-forge/maker-squirrel', config: { name: 'my_app' } },  // Windows
    { name: '@electron-forge/maker-dmg', config: { format: 'ULFO' } },        // macOS
    { name: '@electron-forge/maker-deb', config: { options: { maintainer: 'Me' } } }, // Linux
    { name: '@electron-forge/maker-appx' },                                     // Windows Store
  ],
  plugins: [new VitePlugin()],
};

调试与性能分析

开发调试:

// 主进程调试
// 方法1:VS Code launch.json
// 方法2:命令行
// electron --inspect=5858 .

// 主进程日志
import log from 'electron-log';
log.info('应用启动');
log.error('出错了', error);

// 渲染进程调试
mainWindow.webContents.openDevTools();

性能分析:

// 渲染进程性能
mainWindow.webContents.session.setPreloads([
  join(__dirname, '../preload/perf.js'),
]);

// CPU Profile
mainWindow.webContents.cpuProfiler.start();
// ... 操作 ...
const profile = await mainWindow.webContents.cpuProfiler.stop();
fs.writeFileSync('profile.cpuprofile', JSON.stringify(profile));

// 内存快照
const heap = await mainWindow.webContents.takeHeapSnapshot();

启动速度优化:

// src/main/index.ts
import { app } from 'electron';

// 1. 延迟加载非必要模块
app.whenReady().then(async () => {
  // 2. 首窗口尽快显示
  const win = createWindow('main', { show: false });
  win.once('ready-to-show', () => win.show());

  // 3. 非关键初始化延后
  setImmediate(() => {
    setupAutoUpdater(win);
    createTray(win);
    createAppMenu();
  });
});

// 4. 关闭不需要的 Chromium 特性
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling');
app.commandLine.appendSwitch('disable-background-timer-throttling');

内存优化:

// 及时销毁窗口
win.on('closed', () => {
  win.removeAllListeners();
  win = null;
});

// 限制渲染进程缓存
session.defaultSession.clearCache();

// 检查内存泄漏
setInterval(() => {
  const mem = process.memoryUsage();
  log.info(`RSS: ${(mem.rss / 1024 / 1024).toFixed(1)}MB, Heap: ${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`);
}, 60000);
// 注册自定义协议
app.setAsDefaultProtocolClient('myapp');

// macOS 处理 URL 事件
app.on('open-url', (event, url) => {
  event.preventDefault();
  handleDeepLink(url);
});

// Windows 处理命令行参数
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (_event, argv) => {
    const url = argv.find(arg => arg.startsWith('myapp://'));
    if (url) handleDeepLink(url);
    mainWindow?.show();
  });
}

function handleDeepLink(url: string) {
  // myapp://open?id=123
  const parsed = new URL(url);
  const action = parsed.hostname;
  const params = Object.fromEntries(parsed.searchParams);

  mainWindow?.webContents.send('deep-link', { action, params });
}

常见问题与踩坑

问题原因解决方案
包体积过大打包了完整 Chromiumelectron-builder 配置压缩/NSIS,或考虑 Tauri
内存占用高每个窗口独立渲染进程减少窗口数,用 BrowserView 复用,及时销毁
渲染进程被 XSS 攻击开启了 nodeIntegration必须关闭 nodeIntegration + 开启 contextIsolation
contextBridge 传函数失败序列化限制只暴露函数引用,实际逻辑在 preload 中执行
原生模块编译失败node-gyp 依赖问题用 electron-rebuild 重新编译
macOS 签名公证失败缺少 Apple 开发者证书配置 CSC_LINK/CSC_KEY_PASSWORD,用 notarize
窗口闪烁白屏加载时背景默认白色show: false + backgroundColor + ready-to-show
启动慢Chromium 初始化开销延迟创建窗口,app.whenReady() 优化加载顺序
多窗口通信复杂各渲染进程独立主进程做消息中转,或用 MessagePort
渲染进程崩溃内存泄漏/JS 错误webContents.on('crashed') 监听,自动重启
文件监听失效asar 打包后路径变化process.resourcesPath 获取资源路径
中文路径乱码Windows 编码问题path.normalize 处理,避免 GBK 路径
macOS Dock 点击无反应窗口已隐藏监听 activate 事件重新显示窗口
打包后白屏路径引用错误使用 __dirname + path.join 拼接路径
asar 内文件无法读取某些原生模块不支持 asarunpackDirName 排除,或用 process.noAsar

最佳实践

  • 安全第一:永远关闭 nodeIntegration,开启 contextIsolation + sandbox
  • IPC 优先用 invoke/handle:双向通信用 invoke,比 send/on 更安全(有返回值、错误处理)
  • Preload 最小暴露:只暴露渲染进程真正需要的 API,不暴露整个 ipcRenderer
  • 主进程做重活:文件 I/O、加密、数据库操作放主进程,渲染进程只管 UI
  • 窗口生命周期管理show: false + ready-to-show 避免白屏,关闭时 win = null 释放内存
  • 打包优化:asar 打包、tree-shaking、按平台打包、排除 devDependencies
  • 自动更新:electron-updater + GitHub Releases,启动时检查 + 定时轮询
  • 错误监控webContents.on('crashed') + process.on('uncaughtException') 全局捕获
  • 单实例锁app.requestSingleInstanceLock() 防止多开
  • 日志记录:electron-log 统一管理主进程和渲染进程日志

面试题

Q1: Electron 的主进程和渲染进程有什么区别?它们如何通信?

主进程运行 Node.js,负责创建窗口(BrowserWindow)、管理系统资源和原生交互,每个应用只有一个。渲染进程运行 Chromium,负责页面渲染和 UI 交互,可以有多个(多窗口)。通信通过 IPC:ipcMain.handle / ipcRenderer.invoke 做双向通信,ipcRenderer.send / ipcMain.on 做单向推送,MessagePort 做流式双向通信。

Q2: Electron 中 contextIsolation 和 nodeIntegration 为什么重要?

nodeIntegration: false 禁止渲染进程直接使用 Node.js API,防止 XSS 攻击获得文件系统等能力;contextIsolation: true 让 preload 脚本和渲染页面的 JS 运行在隔离的 V8 上下文中,防止页面脚本篡改 preload 暴露的 API。两者结合是 Electron 最重要的安全防线,关闭任一项都会导致严重安全漏洞。

Q3: Electron 和 Tauri 的核心区别是什么?

Electron 打包 Chromium + Node.js,体积大(~100MB+)但兼容性好、生态成熟;Tauri 用系统自带的 WebView + Rust 后端,体积小(~5MB)性能好但 WebView 兼容性依赖系统、Rust 学习成本高。选 Electron:需要最强兼容性和成熟生态;选 Tauri:追求小体积、低内存和安全性。

Q4: 如何优化 Electron 应用的启动速度和内存占用?

启动优化:show: false 创建窗口等 ready-to-show 再显示、延迟创建非首屏窗口、非关键模块 setImmediate 延后初始化、关闭不需要的 Chromium 特性。内存优化:减少窗口数量、及时销毁关闭的窗口(win = null)、用 BrowserView 替代多窗口、避免渲染进程缓存大量数据、定期 clearCache

Q5: Electron 的 Preload 脚本作用是什么?为什么不直接在渲染进程中使用 Node.js?

Preload 在渲染进程的页面加载前执行,通过 contextBridge.exposeInMainWorld 安全地向渲染进程暴露有限 API。不在渲染进程直接使用 Node.js 是因为:渲染进程加载不可信的网页内容,如果直接拥有 Node.js 能力,XSS 攻击就能读写文件、执行系统命令。Preload 作为中间层,只暴露必要的、经过验证的接口,实现最小权限原则。

Q6: Electron 中 invoke/handle 和 send/on 两种 IPC 方式有什么区别?

invoke/handle 是双向请求-响应模式,返回 Promise,支持错误传播(handler 抛出的异常会传到渲染进程),适合需要返回值的操作(如读取文件)。send/on 是单向事件模式,无返回值,适合通知类场景(如显示通知、状态推送)。推荐:需要返回值用 invoke,纯事件通知用 send + webContents.send。

Q7: Electron 打包后的应用如何实现自动更新?

使用 electron-updater 库。流程:① 在 electron-builder.yml 配置 publish 指向更新源(GitHub Releases/自定义服务器);② 主进程启动时调用 autoUpdater.checkForUpdates();③ 检测到新版本后 autoUpdater.downloadUpdate() 下载;④ 下载完成后通知用户,调用 autoUpdater.quitAndInstall() 重启安装。支持全量更新和差量更新(Windows NSIS)。

Q8: 如何处理 Electron 应用的多窗口架构?

用 Map 管理多个 BrowserWindow 实例,每个窗口有唯一标识。窗口间通信通过主进程中转:A 窗口 → IPC → 主进程 → IPC → B 窗口。也可用 MessagePort 建立直接通道。注意:每个窗口独立渲染进程,要及时销毁(win.close() + win = null),避免内存泄漏。模态窗口通过 parentmodal 属性实现。


相关链接: