Electron 学习笔记

一份完整的 Electron 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),覆盖从入门到打包发布的全链路知识。以 VS Code、Slack 等真实应用为案例。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查 API → 翻 附录 A API 速查
  3. 排错 → 翻 附录 B 常见问题 FAQ
  4. 落地 → 跟 附录 C 实战场景做一遍

1 · Electron 概述与架构

1.1 Electron 是什么

  • Electron 是一个用 JavaScript、HTML、CSS 构建跨平台桌面应用的框架
  • 由 Cheng Zhao(赵成) 于 2013 年在 GitHub 开发,最初叫 Atom Shell,为 GitHub 的 Atom 编辑器而生
  • 2015 年正式更名为 Electron,目前由 OpenJS Foundation 维护
  • 核心组合:Chromium(渲染引擎)+ Node.js(后端运行时)+ 原生 API 桥接层
TIP

📌 打个比方:Electron 像给你一套精装房——自带全套家电(Chromium 浏览器引擎 + Node.js 运行时),拎包入住,装修风格统一。代价是房子比较重(安装包 80+ MB),但省去了"不同系统家具风格不一致"的烦恼。这就是 Electron 和 Tauri 的核心区别:Electron 捆绑一切,Tauri 复用系统 WebView。

1.2 谁在用 Electron

应用 用户量 说明
VS Code 数千万 微软的代码编辑器,Electron 最成功的案例
Slack 数千万 团队协作工具
Discord 上亿 游戏社区语音/文字聊天
Figma Desktop 数百万 设计工具桌面版
Notion 数千万 笔记/知识管理
1Password 数百万 密码管理器
GitHub Desktop 数百万 Git 图形客户端
TIP

📌 为什么大厂爱用 Electron?因为前端团队直接就能写桌面应用,不需要学 C++/Qt/Rust。一套 Web 技术栈 → Windows、macOS、Linux 三端通吃。对初创团队来说,"能快速出产品"比"安装包小 50MB"重要得多。

1.3 整体架构

┌──────────────────────────────────────────────────────────┐ │ Electron 应用 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 主进程 │ │ 渲染进程(xN) │ │ │ │ (Main) │ │ (Renderer) │ │ │ │ │ │ │ │ │ │ Node.js │ │ Chromium │ │ │ │ 完整能力 │ │ 渲染引擎 │ │ │ │ │ │ │ │ │ │ 文件系统 │ │ HTML/CSS/JS │ │ │ │ 窗口管理 │ │ DOM API │ │ │ │ 原生菜单 │ │ 前端框架 │ │ │ │ 系统对话框 │ │ (React/Vue) │ │ │ │ 托盘图标 │ │ │ │ │ │ 自动更新 │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ └───── IPC 通信 ─────┘ │ │ (ipcMain/ipcRenderer) │ │ (contextBridge) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 原生 API 桥接层 │ │ │ │ (C++ 编写的底层模块,连接 JS 与操作系统) │ │ │ └──────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘

每层说明

层 作用 技术栈
主进程 (Main) 管理应用生命周期、窗口、原生 API Node.js + Electron Main API
渲染进程 (Renderer) 渲染 UI、用户交互 Chromium + 前端框架(React/Vue/Svelte…)
IPC 通信层 主进程与渲染进程间消息传递 ipcMain / ipcRenderer / contextBridge
原生 API 桥接 将系统能力暴露给 JS C++ 底层模块
TIP

📌 为什么有两个进程?Chromium 本身就是多进程架构——每个网页标签页是独立的渲染进程。Electron 继承了这个设计:主进程管"壳"(窗口、菜单、系统交互),渲染进程管"内容"(UI、用户交互)。这和浏览器的"浏览器进程 + 标签页进程"是一一对应的。Node.js 能力只在主进程完整可用,渲染进程默认被沙箱隔离。

1.4 Electron vs Tauri 快速对比

维度 Electron Tauri
渲染引擎 捆绑 Chromium 系统自带 WebView
后端运行时 捆绑 Node.js Rust
安装包大小 ~80-150 MB ~3-10 MB
内存占用 较高(Chromium 开销) 较低
API 一致性 所有平台渲染一致 不同系统 WebView 有差异
学习曲线 低(前端开发者直接上手) 中(需要学 Rust 基础)
生态成熟度 非常成熟(2013 至今) 较新(2020+)
原生模块 N-API / node-native-addon Rust 插件
TIP

📌 选型建议:如果你的团队全是前端开发者,需要快速出产品,不在乎包体积——选 Electron。如果你追求极致轻量、安全,或者已经会用 Rust——选 Tauri。两者不是互斥的,很多概念(主进程/渲染进程、IPC 通信)是相通的。


2 · 安装与项目创建

2.1 环境要求

  • Node.js:>= 16(推荐 LTS 版本)
  • npm / yarn / pnpm:任选一个包管理器
  • 操作系统:Windows 10+ / macOS 10.15+ / Ubuntu 18.04+

2.2 方式一:electron-quick-start(最简模板)

# 克隆官方模板
git clone https://github.com/electron/electron-quick-start
cd electron-quick-start

# 安装依赖
npm install

# 启动
npm start

2.3 方式二:electron-forge(推荐脚手架)

# 用 Electron Forge 创建项目
npx @electron-forge/cli init my-electron-app

# 进入项目
cd my-electron-app

# 启动
npm start

2.4 方式三:用 Vite + Electron(现代前端工具链)

# 创建 Vite + React + Electron 项目
npm create @quick-start/electron@latest my-app -- --template react-ts

cd my-app
npm install
npm run dev
TIP

📌 为什么推荐 electron-forge?因为它集成了打包、自动更新、代码签名等全流程工具,不需要你再手动拼凑 electron-builder + electron-updater + squirrel。Forge 是 Electron 官方推荐的"一站式"工具链。

2.5 package.json 关键配置

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "src/main/index.js",
  "scripts": {
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make",
    "publish": "electron-forge publish"
  },
  "devDependencies": {
    "electron": "^30.0.0",
    "@electron-forge/cli": "^7.0.0"
  }
}
WARNING

🚧 main 字段是 Electron 的入口点。Electron 启动时首先执行 main 指向的文件(主进程)。如果忘了配,Electron 会默认找 index.js,找不到就报错。


3 · 项目结构详解

3.1 典型项目结构

my-electron-app/ ├── package.json # 项目配置 + Electron 入口 ├── src/ │ ├── main/ │ │ └── index.js # 主进程入口 │ ├── preload/ │ │ └── index.js # 预加载脚本(contextBridge) │ └── renderer/ │ ├── index.html # 渲染进程 HTML │ ├── index.js # 渲染进程 JS │ └── styles.css # 样式 ├── forge.config.js # Electron Forge 配置 └── assets/ └── icon.png # 应用图标

3.2 三个入口文件的关系

Electron 启动 │ ▼ ┌─────────────┐ 创建 BrowserWindow ┌──────────────┐ │ 主进程 │ ──────────────────────────▶ │ 渲染进程 │ │ main.js │ │ index.html │ │ │ 加载 preload 脚本 │ │ │ Node.js │ ──────────────────────────▶ │ + preload.js│ │ 完整能力 │ │ (在页面加载 │ │ │ ◄────── IPC 通信 ──────────▶ │ 前执行) │ └─────────────┘ └──────────────┘
TIP

📌 preload 脚本是安全桥梁。它在渲染进程的页面加载之前执行,有受限的 Node.js 访问权。通过 contextBridge 暴露安全的 API 给渲染进程,避免渲染进程直接接触 Node.js——这是 Electron 安全模型的核心。


4 · 主进程与渲染进程

4.1 主进程 (Main Process)

主进程是 Electron 的"大脑",负责:

  • 应用生命周期:app.whenReady()、app.on('window-all-closed')、app.quit()
  • 窗口管理:创建/销毁 BrowserWindow
  • 原生 UI:菜单 (Menu)、托盘 (Tray)、对话框 (dialog)
  • 系统 API:文件系统、剪贴板、电源状态、屏幕信息
  • Node.js 完整能力:fs、path、http、child_process 等所有 Node 模块
// src/main/index.js
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,   // 安全:隔离上下文
      nodeIntegration: false,   // 安全:禁用 Node.js in renderer
    },
  });

  // 开发环境加载 dev server,生产环境加载打包文件
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:5173');
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.whenReady().then(() => {
  createWindow();

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

// 非 macOS: 所有窗口关闭时退出应用
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

4.2 渲染进程 (Renderer Process)

渲染进程就是"网页",负责 UI 渲染和用户交互:

  • Chromium 渲染引擎:完整的 HTML/CSS/JS 能力
  • DOM API:document、window、fetch、Canvas 等
  • 前端框架:React、Vue、Svelte 等均可使用
  • 默认无 Node.js:出于安全考虑,nodeIntegration: false
<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>My Electron App</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div id="app">
    <h1>Hello Electron!</h1>
    <button id="btn">读取文件</button>
    <pre id="output"></pre>
  </div>
  <script src="index.js"></script>
</body>
</html>
// src/renderer/index.js
// 渲染进程通过 window.api 调用 preload 暴露的安全 API
document.getElementById('btn').addEventListener('click', async () => {
  const result = await window.api.readFile('example.txt');
  document.getElementById('output').textContent = result;
});

4.3 Preload 脚本与 contextBridge

// src/preload/index.js
const { contextBridge, ipcRenderer } = require('electron');

// 通过 contextBridge 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('api', {
  // 异步读取文件(通过 IPC 转发给主进程)
  readFile: (filename) => ipcRenderer.invoke('read-file', filename),
  // 监听主进程发来的事件
  onFileChanged: (callback) => ipcRenderer.on('file-changed', callback),
  // 获取应用版本
  getVersion: () => ipcRenderer.invoke('get-version'),
});
TIP

📌 为什么不能直接在渲染进程用 Node.js?因为渲染进程加载的网页可能包含第三方内容(npm 包、CDN 脚本),如果开启了 nodeIntegration,恶意脚本就能通过 require('child_process') 执行系统命令——这是 2018 年前 Electron 应用的常见安全漏洞。contextIsolation + preload + contextBridge 是 Electron 官方的安全最佳实践。

4.4 进程通信全景

┌─────────────────────────────────────────────────────────┐ │ 主进程 │ │ │ │ ipcMain.handle('read-file', handler) // 异步请求-响应 │ │ ipcMain.on('notify', handler) // 异步单向 │ │ mainWindow.webContents.send('update') // 主动推送 │ │ │ └──────────────────────┬──────────────────────────────────┘ │ IPC 通道 │ ┌──────────────────────┴──────────────────────────────────┐ │ Preload 脚本 │ │ │ │ contextBridge.exposeInMainWorld('api', { │ │ readFile: (name) => ipcRenderer.invoke('read-file'), │ │ onUpdate: (cb) => ipcRenderer.on('update', cb), │ │ }) │ │ │ └──────────────────────┬──────────────────────────────────┘ │ window.api 对象 │ ┌──────────────────────┴──────────────────────────────────┐ │ 渲染进程 │ │ │ │ await window.api.readFile('data.txt') // 调用主进程 │ │ window.api.onUpdate((data) => {...}) // 接收推送 │ │ │ └─────────────────────────────────────────────────────────┘

5 · 窗口管理

5.1 BrowserWindow 基础

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

const win = new BrowserWindow({
  width: 1200,
  height: 800,
  minWidth: 800,
  minHeight: 600,
  maxWidth: 1920,
  maxHeight: 1080,
  x: 100,           // 窗口初始 x 位置
  y: 100,           // 窗口初始 y 位置
  resizable: true,
  movable: true,
  minimizable: true,
  maximizable: true,
  closable: true,
  focusable: true,
  fullscreenable: true,
  title: 'My App',
  icon: '/path/to/icon.png',
  frame: true,       // 是否显示标题栏
  transparent: false, // 是否透明
  alwaysOnTop: false, // 是否置顶
  skipTaskbar: false, // 是否在任务栏显示
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
  },
});

5.2 无边框窗口(自定义标题栏)

const win = new BrowserWindow({
  frame: false,       // 无原生标题栏
  titleBarStyle: 'hidden', // macOS: 保留交通灯按钮
  webPreferences: { ... },
});
TIP

📌 VS Code 就是用无边框窗口 + 自定义标题栏。这样可以在标题栏放命令面板、搜索框等自定义 UI。代价是你得自己实现拖拽区域(-webkit-app-region: drag)和窗口控制按钮(最小化/最大化/关闭)。

/* 自定义标题栏拖拽区域 */
.titlebar {
  -webkit-app-region: drag;     /* 可拖拽 */
  height: 32px;
  background: #1e1e1e;
}

.titlebar button {
  -webkit-app-region: no-drag;  /* 按钮可点击 */
}
// 渲染进程中的窗口控制按钮
document.getElementById('minimize').addEventListener('click', () => {
  window.api.minimize();
});
document.getElementById('maximize').addEventListener('click', () => {
  window.api.maximize();
});
document.getElementById('close').addEventListener('click', () => {
  window.api.close();
});

5.3 多窗口管理

// 主进程:多窗口管理器
const windows = new Map();

function createWindow(name, options) {
  if (windows.has(name)) {
    windows.get(name).focus();
    return windows.get(name);
  }

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

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

  return win;
}

// 主窗口
createWindow('main', { width: 1200, height: 800 });

// 设置窗口
createWindow('settings', {
  width: 600,
  height: 400,
  parent: windows.get('main'), // 模态窗口
  modal: true,
});

5.4 窗口间通信

// 主进程转发消息
ipcMain.on('message-to-main', (event, message) => {
  // 转发给主窗口
  windows.get('main').webContents.send('message-from-sub', message);
});

// 或者用 BroadcastChannel(渲染进程间直接通信)
// 渲染进程 A
const bc = new BroadcastChannel('app-channel');
bc.postMessage({ type: 'update', data: 'hello' });

// 渲染进程 B
const bc = new BroadcastChannel('app-channel');
bc.onmessage = (event) => {
  console.log('收到:', event.data);
};

6 · IPC 通信

6.1 ipcRenderer.invoke / ipcMain.handle(推荐)

异步请求-响应模式,类似 HTTP 请求:

// 主进程
const { ipcMain, dialog } = require('electron');
const fs = require('fs');

ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
  });
  if (result.canceled) return null;
  return fs.readFileSync(result.filePaths[0], 'utf-8');
});

// preload
contextBridge.exposeInMainWorld('api', {
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
});

// 渲染进程
const content = await window.api.openFile();
console.log(content);

6.2 ipcRenderer.send / ipcMain.on(单向通知)

渲染进程 → 主进程的单向消息:

// 主进程
ipcMain.on('app:minimize', () => {
  BrowserWindow.getFocusedWindow()?.minimize();
});

// preload
contextBridge.exposeInMainWorld('api', {
  minimize: () => ipcRenderer.send('app:minimize'),
});

// 渲染进程
document.getElementById('min-btn').addEventListener('click', () => {
  window.api.minimize();
});

6.3 主进程 → 渲染进程推送

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

// preload
contextBridge.exposeInMainWorld('api', {
  onUpdateAvailable: (callback) => {
    ipcRenderer.on('update:available', (_, data) => callback(data));
  },
});

// 渲染进程
window.api.onUpdateAvailable((data) => {
  showNotification(`新版本 ${data.version} 可用!`);
});

6.4 IPC 通信模式总结

模式 主进程 渲染进程 场景
请求-响应 ipcMain.handle() ipcRenderer.invoke() 读文件、调系统 API
单向通知 R→M ipcMain.on() ipcRenderer.send() 最小化、退出等操作
单向通知 M→R webContents.send() ipcRenderer.on() 推送更新、状态变化
双向通信 ipcMain.on() + event.reply() ipcRenderer.send() + ipcRenderer.on() 长连接式通信
TIP

📌 为什么用 invoke/handle 而不是 send/on?因为 invoke 返回 Promise,天然支持 async/await,错误也能通过 Promise rejection 传递。send/on 是事件驱动的,需要手动管理回调,容易遗漏错误处理。新代码一律用 invoke/handle。


7 · 菜单与托盘

7.1 应用菜单

const { Menu } = require('electron');

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: () => createNewDocument(),
      },
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: async () => {
          const { filePaths } = await dialog.showOpenDialog({
            properties: ['openFile'],
          });
          if (filePaths[0]) openFile(filePaths[0]);
        },
      },
      { type: 'separator' },
      {
        label: '退出',
        accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4',
        role: 'quit',
      },
    ],
  },
  {
    label: '编辑',
    submenu: [
      { role: 'undo', label: '撤销' },
      { role: 'redo', label: '重做' },
      { type: 'separator' },
      { role: 'cut', label: '剪切' },
      { role: 'copy', label: '复制' },
      { role: 'paste', label: '粘贴' },
      { role: 'selectAll', label: '全选' },
    ],
  },
  {
    label: '视图',
    submenu: [
      { role: 'reload', label: '重新加载' },
      { role: 'forceReload', label: '强制重新加载' },
      { role: 'toggleDevTools', label: '开发者工具' },
      { type: 'separator' },
      { role: 'resetZoom', label: '重置缩放' },
      { role: 'zoomIn', label: '放大' },
      { role: 'zoomOut', label: '缩小' },
      { type: 'separator' },
      { role: 'togglefullscreen', label: '全屏' },
    ],
  },
];

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

7.2 macOS 特殊处理

TIP

📌 macOS 的菜单栏在屏幕顶部,不是在窗口上。所以即使没有打开窗口,菜单栏也要存在。Electron 通过 app.dock 和菜单的 role: 'appMenu' 处理这个差异。第一个菜单项在 macOS 上会变成应用名菜单(包含"关于"、"退出"等)。

if (process.platform === 'darwin') {
  // macOS: 第一个菜单是应用菜单
  template.unshift({
    label: app.getName(),
    submenu: [
      { role: 'about' },
      { type: 'separator' },
      { role: 'services' },
      { type: 'separator' },
      { role: 'hide' },
      { role: 'hideOthers' },
      { role: 'unhide' },
      { type: 'separator' },
      { role: 'quit' },
    ],
  });
}

7.3 系统托盘

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

let tray;

function createTray() {
  const icon = nativeImage.createFromPath(path.join(__dirname, 'assets/tray-icon.png'));
  // macOS: 托盘图标应该是模板图片(黑白透明)
  icon.setTemplateImage(true);

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

  const contextMenu = Menu.buildFromTemplate([
    { label: '显示窗口', click: () => mainWindow.show() },
    { label: '设置', click: () => openSettings() },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() },
  ]);

  tray.setContextMenu(contextMenu);

  // 点击托盘图标
  tray.on('click', () => {
    if (mainWindow.isVisible()) {
      mainWindow.hide();
    } else {
      mainWindow.show();
    }
  });
}
WARNING

🚧 托盘图标要清理。应用退出时如果不销毁 Tray 实例,托盘图标会残留在系统托盘区(Windows 上尤其明显)。记得在 app.on('before-quit') 中 tray.destroy()。


8 · 对话框与文件操作

8.1 文件对话框

const { dialog } = require('electron');

// 打开文件
async function openFileDialog() {
  const result = await dialog.showOpenDialog(mainWindow, {
    title: '选择文件',
    defaultPath: app.getPath('documents'),
    properties: ['openFile', 'multiSelections'],
    filters: [
      { name: '图片', extensions: ['jpg', 'png', 'gif', 'webp'] },
      { name: '所有文件', extensions: ['*'] },
    ],
  });
  if (!result.canceled) {
    console.log('选中文件:', result.filePaths);
  }
}

// 保存文件
async function saveFileDialog() {
  const result = await dialog.showSaveDialog(mainWindow, {
    title: '保存文件',
    defaultPath: path.join(app.getPath('documents'), 'untitled.txt'),
    filters: [
      { name: '文本文件', extensions: ['txt'] },
      { name: 'Markdown', extensions: ['md'] },
    ],
  });
  if (!result.canceled) {
    fs.writeFileSync(result.filePath, content, 'utf-8');
  }
}

8.2 消息框

// 显示消息框
const result = await dialog.showMessageBox(mainWindow, {
  type: 'warning',
  title: '确认删除',
  message: '确定要删除这个文件吗?',
  detail: '此操作不可撤销。',
  buttons: ['删除', '取消'],
  defaultId: 1,    // 默认聚焦"取消"
  cancelId: 1,     // ESC 键对应"取消"
});

if (result.response === 0) {
  // 用户点了"删除"
  deleteFile();
}

8.3 文件操作(主进程)

const fs = require('fs/promises');
const path = require('path');
const { app } = require('electron');

// Electron 提供的路径 API
const userDataPath = app.getPath('userData');    // 用户数据目录
const documentsPath = app.getPath('documents');   // 文档目录
const downloadsPath = app.getPath('downloads');   // 下载目录
const tempPath = app.getPath('temp');             // 临时目录

// 读写配置文件
async function readConfig() {
  const configPath = path.join(userDataPath, 'config.json');
  try {
    const data = await fs.readFile(configPath, 'utf-8');
    return JSON.parse(data);
  } catch {
    return { theme: 'dark', language: 'zh' }; // 默认值
  }
}

async function writeConfig(config) {
  const configPath = path.join(userDataPath, 'config.json');
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
}
TIP

📌 为什么文件操作在主进程做?因为渲染进程默认没有 Node.js 的 fs 模块(nodeIntegration: false)。文件操作通过 IPC 转发给主进程执行,主进程有完整的 Node.js 能力。这既是安全隔离,也是职责分离——渲染进程管 UI,主进程管系统操作。


9 · 快捷键与命令

9.1 全局快捷键

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

app.whenReady().then(() => {
  // 注册全局快捷键(应用未聚焦时也生效)
  globalShortcut.register('CommandOrControl+Shift+D', () => {
    mainWindow.show();
    mainWindow.focus();
  });

  globalShortcut.register('CommandOrControl+Alt+S', () => {
    takeScreenshot();
  });
});

// 应用退出时取消注册
app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});
WARNING

🚧 全局快捷键要谨慎。它在你应用没聚焦时也生效,可能和系统快捷键或其他应用冲突。注册前检查返回值:const ret = globalShortcut.register(...); if (!ret) console.log('注册失败')。

9.2 应用内快捷键(Menu accelerator)

// 通过菜单项的 accelerator 属性
{
  label: '保存',
  accelerator: 'CmdOrCtrl+S',
  click: () => saveDocument(),
}

// 或在渲染进程中监听键盘事件
document.addEventListener('keydown', (e) => {
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault();
    saveDocument();
  }
});

9.3 剪贴板

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

// 主进程
function copyToClipboard(text) {
  clipboard.writeText(text);
}

function readClipboard() {
  return clipboard.readText();
}

// 图片
const image = clipboard.readImage();
if (!image.isEmpty()) {
  // 保存图片到文件
  const pngBuffer = image.toPNG();
  fs.writeFileSync('screenshot.png', pngBuffer);
}

10 · 持久化存储

10.1 electron-store(简单键值存储)

npm install electron-store
const Store = require('electron-store');

const store = new Store({
  defaults: {
    windowBounds: { width: 1200, height: 800 },
    recentFiles: [],
    settings: {
      theme: 'dark',
      fontSize: 14,
      autoSave: true,
    },
  },
});

// 读取
const theme = store.get('settings.theme'); // 'dark'

// 写入
store.set('settings.theme', 'light');

// 删除
store.delete('settings.fontSize');

// 记住窗口位置
mainWindow.on('close', () => {
  const bounds = mainWindow.getBounds();
  store.set('windowBounds', bounds);
});

10.2 SQLite(结构化数据)

npm install better-sqlite3
const Database = require('better-sqlite3');

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

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

// 插入
const insert = db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)');
const info = insert.run('我的笔记', '内容...');
console.log('插入 ID:', info.lastInsertRowid);

// 查询
const selectAll = db.prepare('SELECT * FROM notes ORDER BY updated_at DESC');
const notes = selectAll.all();
TIP

📌 为什么用 better-sqlite3 而不是 sqlite3?better-sqlite3 是同步 API,代码更简洁,性能更好(C++ 绑定,不需要 Promise 开销)。在主进程中用同步数据库操作完全没问题——主进程不是 UI 线程,不会卡 UI。

10.3 localStorage / IndexedDB(渲染进程)

// 渲染进程中可直接用 Web API
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');

// IndexedDB 适合大量结构化数据
const request = indexedDB.open('MyAppDB', 1);
request.onupgradeneeded = (e) => {
  const db = e.target.result;
  db.createObjectStore('files', { keyPath: 'id' });
};

11 · 系统集成 API

11.1 系统通知

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

function showNotification(title, body) {
  if (Notification.isSupported()) {
    new Notification({
      title,
      body,
      icon: path.join(__dirname, 'assets/icon.png'),
      silent: false,
    }).show();
  }
}

// 渲染进程也可以用 Web Notification API
// new Notification('标题', { body: '内容' });

11.2 电源状态

const { powerMonitor } = require('electron');

powerMonitor.on('on-ac', () => {
  console.log('切换到交流电源');
  // 可以暂停省电模式,恢复后台任务
});

powerMonitor.on('on-battery', () => {
  console.log('切换到电池供电');
  // 降低后台任务频率
});

powerMonitor.on('suspend', () => {
  console.log('系统即将休眠');
  // 保存状态
});

powerMonitor.on('resume', () => {
  console.log('系统从休眠恢复');
});

// 获取电池信息
const batteryLevel = powerMonitor.getSystemIdleTime();

11.3 屏幕信息

const { screen } = require('electron');

// 获取所有显示器
const displays = screen.getAllDisplays();
displays.forEach((display, i) => {
  console.log(`显示器 ${i}: ${display.bounds.width}x${display.bounds.height}`);
  console.log(`  位置: (${display.bounds.x}, ${display.bounds.y})`);
  console.log(`  缩放: ${display.scaleFactor}`);
});

// 获取鼠标位置
const cursorPos = screen.getCursorScreenPoint();

// 在鼠标所在的显示器上显示窗口
const currentDisplay = screen.getDisplayNearestPoint(cursorPos);
win.setPosition(currentDisplay.bounds.x + 100, currentDisplay.bounds.y + 100);

11.4 文件关联与深链接

// 注册文件关联(package.json 或 forge.config.js)
// forge.config.js
module.exports = {
  packagerConfig: {
    fileAssociations: [
      { ext: 'myapp', name: 'My App File', role: 'Editor' },
    ],
    protocols: [
      { name: 'My App Protocol', schemes: ['myapp'] },
    ],
  },
};

// 主进程处理文件打开
app.on('open-file', (event, path) => {
  event.preventDefault();
  openFile(path);
});

// 处理深链接
app.on('open-url', (event, url) => {
  event.preventDefault();
  console.log('深链接:', url); // myapp://page/settings
});

12 · 性能优化

12.1 减少启动时间

// 1. 延迟加载非关键模块
const lazyLoad = (module) => {
  let cached;
  return () => cached || (cached = require(module));
};
const getDatabase = lazyLoad('./database');

// 2. 使用 v8-compile-cache
require('v8-compile-cache');

// 3. 分离主进程和渲染进程的打包
// 主进程只打包 Node.js 依赖,渲染进程用 Vite/Webpack

12.2 减少内存占用

// 1. 及时销毁不需要的窗口
win.on('closed', () => {
  win = null; // 让 GC 回收
});

// 2. 限制渲染进程数量
// 能用标签页就不用新窗口
const { session } = require('electron');

// 3. 清理 session 缓存
session.defaultSession.clearCache();
session.defaultSession.clearStorageData({
  storages: ['cookies', 'localstorage'],
});

// 4. 启用 Chromium 垃圾回收
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=256');

12.3 渲染进程优化

// 1. 使用 requestAnimationFrame 而非 setInterval 做动画
// 2. 大列表用虚拟滚动(react-window / vue-virtual-scroller)
// 3. 图片懒加载
// 4. 避免同步 IPC 调用(ipcRenderer.sendSync 会阻塞渲染进程)

// 5. 使用 webContents 的 backgroundThrottling
win.webContents.backgroundThrottling = true; // 后台时降低 FPS
TIP

📌 Electron 性能优化的核心思路:Electron 本质是 Chromium + Node.js,所以 Web 性能优化技巧全部适用(虚拟滚动、代码分割、懒加载等)。额外注意的是 IPC 通信开销——每次 invoke 都是跨进程序列化/反序列化,高频调用会成为瓶颈。批量传递数据,避免在循环中调用 IPC。


13 · 安全策略

13.1 安全清单

配置项 推荐值 说明
nodeIntegration false 渲染进程禁用 Node.js
contextIsolation true 隔离 preload 和渲染进程上下文
sandbox true 启用 Chromium 沙箱
webSecurity true 启用同源策略
allowRunningInsecureContent false 禁止加载 HTTP 资源
preload 指定 preload 脚本 通过 contextBridge 暴露安全 API

13.2 安全的 BrowserWindow 配置

const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,      // 隔离上下文
    nodeIntegration: false,      // 禁用 Node.js
    sandbox: true,               // 启用沙箱
    webSecurity: true,           // 同源策略
    allowRunningInsecureContent: false, // 禁止 HTTP
    spellcheck: true,
  },
});

13.3 验证 IPC 消息

// 主进程:验证来自渲染进程的消息
ipcMain.handle('file:read', (event, filePath) => {
  // 验证 sender
  const senderWindow = BrowserWindow.fromWebContents(event.sender);
  if (!senderWindow) return;

  // 验证文件路径(防止路径穿越攻击)
  const allowedDir = app.getPath('userData');
  const resolvedPath = path.resolve(filePath);
  if (!resolvedPath.startsWith(allowedDir)) {
    throw new Error('不允许访问此路径');
  }

  return fs.readFileSync(resolvedPath, 'utf-8');
});
WARNING

🚧 永远不要用 ipcRenderer.on 直接暴露给网页。如果渲染进程加载了第三方内容(比如广告、用户生成内容),恶意脚本可以监听你的 IPC 通道。始终通过 contextBridge 精确暴露需要的 API。

13.4 Content Security Policy (CSP)

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self';
               script-src 'self';
               style-src 'self' 'unsafe-inline';
               img-src 'self' data:;
               connect-src 'self' https://api.example.com;">

14 · 打包与分发

14.1 Electron Forge 打包

# 打包(不生成安装包,只生成可执行目录)
npm run package

# 生成安装包(make)
npm run make

14.2 forge.config.js 配置

module.exports = {
  packagerConfig: {
    name: 'MyApp',
    executableName: 'myapp',
    icon: 'assets/icon',
    asar: true,             // 打包为 asar 归档
    prune: true,            // 移除 devDependencies
    overwrite: true,
    // 平台特定配置
    win: {
      target: ['nsis'],
    },
    mac: {
      target: ['dmg'],
      category: 'public.app-category.productivity',
    },
    linux: {
      target: ['AppImage', 'deb'],
      category: 'Utility',
    },
  },
  makers: [
    {
      name: '@electron-forge/maker-squirrel',
      config: {
        name: 'myapp',
        setupIcon: 'assets/setup-icon.ico',
      },
    },
    {
      name: '@electron-forge/maker-zip',
      platforms: ['darwin'],
    },
    {
      name: '@electron-forge/maker-dmg',
      config: {},
    },
    {
      name: '@electron-forge/maker-deb',
      config: {},
    },
    {
      name: '@electron-forge/maker-appx',
      config: {},
    },
  ],
};

14.3 各平台安装包格式

平台 格式 说明
Windows .exe (NSIS/Squirrel) 最常用,NSIS 支持自定义安装界面
Windows .msi (WiX) 企业部署友好
Windows .appx Microsoft Store
macOS .dmg 磁盘镜像,拖拽安装
macOS .pkg 安装器
Linux .AppImage 免安装,单文件运行
Linux .deb Debian/Ubuntu
Linux .rpm Fedora/RHEL
Linux .snap Snap Store

14.4 asar 归档

// asar 把你的源码打包成单个文件,防止用户直接看到源码
// 注意:asar 不是加密,只是归档,可以用 asar extract 解包

// forge.config.js 中设置
packagerConfig: {
  asar: true,
  // 或自定义 asar 配置
  asar: {
    unpackDir: 'node_modules/native-module', // 排除原生模块
  },
}
TIP

📌 为什么 asar 不加密?Electron 的哲学是"开源友好"。asar 的目的是减少文件数量(从几百个文件变成一个),加快启动速度和减少文件系统调用。如果你需要保护源码,考虑用 V8 字节码编译(bytenode)或 WebAssembly。

14.5 代码签名

// forge.config.js
packagerConfig: {
  // Windows
  certificateFile: 'cert/cert.pfx',
  certificatePassword: process.env.CERT_PASSWORD,

  // macOS
  osxSign: {
    identity: 'Developer ID Application: Your Name (XXXXXXXXXX)',
    hardenedRuntime: true,
    entitlements: 'entitlements.mac.plist',
  },
}
WARNING

🚧 不签名的后果:Windows 上 SmartScreen 会拦截,macOS 上 Gatekeeper 会阻止运行。用户看到"未知发布者"警告,大概率不会安装。签名是分发的必经步骤。


15 · 自动更新

15.1 使用 electron-updater

npm install electron-updater
// src/main/updater.js
const { autoUpdater } = require('electron-updater');
const { dialog } = require('electron');

autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;

function setupAutoUpdater() {
  autoUpdater.on('update-available', (info) => {
    dialog.showMessageBox({
      type: 'info',
      title: '发现新版本',
      message: `新版本 ${info.version} 可用,是否下载?`,
      buttons: ['下载', '稍后'],
    }).then((result) => {
      if (result.response === 0) {
        autoUpdater.downloadUpdate();
      }
    });
  });

  autoUpdater.on('update-downloaded', () => {
    dialog.showMessageBox({
      type: 'info',
      title: '更新已下载',
      message: '更新已下载完成,重启应用以安装?',
      buttons: ['重启', '稍后'],
    }).then((result) => {
      if (result.response === 0) {
        autoUpdater.quitAndInstall();
      }
    });
  });

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

  // 检查更新
  autoUpdater.checkForUpdates();
}

module.exports = { setupAutoUpdater };

15.2 发布更新

# 用 Forge 发布到 GitHub Releases
npx electron-forge publish --platform=win32 --arch=x64

# forge.config.js
publishers: [
  {
    name: '@electron-forge/publisher-github',
    config: {
      repository: {
        owner: 'your-name',
        name: 'your-repo',
      },
      prerelease: false,
    },
  },
],
TIP

📌 electron-updater 的工作原理:它从你指定的 URL(如 GitHub Releases)下载 latest.yml(Windows)或 latest-mac.yml(macOS),对比版本号,如果有新版就下载增量更新包。你只需要每次发版把安装包和 yml 文件上传到 Releases。


16 · 调试与测试

16.1 调试主进程

# 方法一:用 Chrome DevTools 调试主进程
electron --inspect=5858 .

# 然后在 Chrome 打开 chrome://inspect,点击 "configure" 添加 localhost:5858

# 方法二:VS Code launch.json
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Electron Main",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "runtimeArgs": ["--inspect=5858", "."],
      "port": 5858,
    }
  ]
}

16.2 调试渲染进程

// 主进程代码中自动打开 DevTools
mainWindow.webContents.openDevTools();

// 或快捷键:Ctrl+Shift+I / Cmd+Option+I

16.3 测试

# 安装测试工具
npm install --save-dev mocha spectron
// test/app.test.js(Spectron 已停止维护,推荐用 Playwright)
const { test, expect } = require('@playwright/test');
const { _electron: electron } = require('playwright');

test('app launches and shows title', async () => {
  const electronApp = await electron.launch({ args: ['.'] });
  const window = await electronApp.firstWindow();

  const title = await window.textContent('h1');
  expect(title).toBe('Hello Electron!');

  await electronApp.close();
});
TIP

📌 Spectron 已废弃。Electron 官方推荐用 Playwright 的 Electron 支持来写 E2E 测试。Playwright 可以启动 Electron 应用、模拟用户操作、截图、断言 DOM。


17 · Electron vs Tauri 深度对比

17.1 架构对比

Electron 架构: ┌─────────────────────────────────────────┐ │ Electron 应用 │ │ ┌──────────┐ ┌──────────┐ │ │ │ 主进程 │ │ 渲染进程 │ │ │ │ Node.js │ │ Chromium │ │ │ │ ~30MB │ │ ~120MB │ │ │ └──────────┘ └──────────┘ │ │ 总体积:~150MB+ │ └─────────────────────────────────────────┘ Tauri 架构: ┌─────────────────────────────────────────┐ │ Tauri 应用 │ │ ┌──────────┐ ┌──────────┐ │ │ │ Rust 后端 │ │ 渲染进程 │ │ │ │ ~3MB │ │ 系统 │ │ │ │ │ │ WebView │ │ │ └──────────┘ └──────────┘ │ │ 总体积:~8MB │ └─────────────────────────────────────────┘

17.2 什么时候选 Electron

  • 团队全是前端开发者,不想学 Rust
  • 需要 Web API 100% 一致性(不依赖系统 WebView 差异)
  • 需要丰富的 Node.js 生态(npm 包)
  • 项目对安装包大小不敏感(企业内部工具)
  • 需要成熟的自动更新方案

17.3 什么时候选 Tauri

  • 追求极致轻量和低内存
  • 安全性要求高(Rust 内存安全)
  • 需要移动端支持(Tauri 2.0)
  • 团队已有 Rust 经验
  • 嵌入式或资源受限场景

18 · 与前端框架集成

18.1 Electron + React + Vite

# 创建项目
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

# 安装 Electron
npm install --save-dev electron electron-forge
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  base: './', // Electron 需要相对路径
  build: {
    outDir: 'dist-renderer',
  },
  server: {
    port: 5173,
  },
});

// electron/main.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';

let win: BrowserWindow;

app.whenReady().then(() => {
  win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
    },
  });

  if (process.env.NODE_ENV === 'development') {
    win.loadURL('http://localhost:5173');
    win.webContents.openDevTools();
  } else {
    win.loadFile(path.join(__dirname, '../dist-renderer/index.html'));
  }
});

18.2 Electron + Vue + Vite

# 使用 electron-vite 脚手架
npm create @quick-start/electron@latest my-app -- --template vue-ts

18.3 统一的项目结构(electron-vite)

my-app/ ├── electron/ │ ├── main/ │ │ └── index.ts # 主进程 │ ├── preload/ │ │ └── index.ts # 预加载脚本 │ └── electron-builder.yml ├── src/ # 渲染进程(前端代码) │ ├── App.vue / App.tsx │ ├── main.ts │ └── components/ ├── electron.vite.config.ts └── package.json

19 · 原生模块与 N-API

19.1 使用原生 Node 模块

# 安装原生模块(如 better-sqlite3)
npm install better-sqlite3

# 打包时需要为 Electron 重新编译
npx electron-rebuild

# 或在 package.json 中配置 postinstall
// package.json
{
  "scripts": {
    "postinstall": "electron-rebuild"
  }
}

19.2 N-API 编写原生模块

// native/addon.cc
#include <napi.h>

Napi::String GetVersion(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "1.0.0");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "getVersion"),
              Napi::Function::New(env, GetVersion));
  return exports;
}

NODE_API_MODULE(addon, Init)
// 使用原生模块
const addon = require('./build/Release/addon');
console.log(addon.getVersion()); // '1.0.0'
TIP

📌 什么时候需要原生模块?当你需要调用系统底层 API(如 Windows Registry、macOS Keychain)、复用 C/C++ 库(如 OpenCV)、或做高性能计算时。但原生模块会增加编译复杂度(需要为每个平台编译),能用纯 JS 解决的就别用原生模块。


附录 A · API 速查

A.1 主进程核心 API

API 说明 示例
app.whenReady() 应用就绪 app.whenReady().then(init)
app.quit() 退出应用 app.quit()
app.getPath(name) 获取系统路径 app.getPath('userData')
BrowserWindow 创建窗口 new BrowserWindow({...})
Menu.buildFromTemplate() 构建菜单 Menu.setApplicationMenu(menu)
Tray 系统托盘 new Tray(icon)
dialog.showOpenDialog() 文件选择 await dialog.showOpenDialog(...)
dialog.showSaveDialog() 保存对话框 await dialog.showSaveDialog(...)
dialog.showMessageBox() 消息框 await dialog.showMessageBox(...)
globalShortcut.register() 全局快捷键 globalShortcut.register('Cmd+Shift+D', fn)
clipboard.writeText() 写剪贴板 clipboard.writeText('hello')
Notification 系统通知 new Notification({title, body}).show()
powerMonitor 电源监听 powerMonitor.on('suspend', fn)
screen.getAllDisplays() 显示器信息 screen.getAllDisplays()
shell.openExternal() 打开外部链接 shell.openExternal('https://...')
shell.showItemInFolder() 在文件管理器中显示 shell.showItemInFolder(path)

A.2 IPC 通信 API

API 进程 说明
ipcMain.handle(channel, handler) 主 注册异步处理器
ipcMain.on(channel, handler) 主 监听单向消息
ipcRenderer.invoke(channel, ...args) 渲染 调用主进程(返回 Promise)
ipcRenderer.send(channel, ...args) 渲染 发送单向消息
webContents.send(channel, ...args) 主 向渲染进程推送
contextBridge.exposeInMainWorld(name, api) Preload 暴露安全 API

A.3 BrowserWindow 常用选项

选项 类型 说明
width / height number 窗口尺寸
minWidth / minHeight number 最小尺寸
x / y number 窗口位置
frame boolean 是否显示标题栏
transparent boolean 是否透明
alwaysOnTop boolean 是否置顶
fullscreen boolean 是否全屏
resizable boolean 是否可调整大小
parent BrowserWindow 父窗口
modal boolean 是否模态
webPreferences.preload string preload 脚本路径
webPreferences.contextIsolation boolean 上下文隔离
webPreferences.nodeIntegration boolean 渲染进程 Node.js
webPreferences.sandbox boolean 沙箱模式

附录 B · 常见问题 FAQ

Q1: 为什么我的 Electron 应用安装包这么大?

A: Electron 捆绑了完整的 Chromium 和 Node.js 运行时,基础包约 80-150MB。优化方法:

  • 用 asar 打包减少文件数量
  • prune: true 移除 devDependencies
  • 用 electron-builder 的 compression: maximum
  • 考虑用 Tauri 替代(如果包大小是硬需求)

Q2: 渲染进程报错 require is not defined

A: nodeIntegration: false 时渲染进程没有 Node.js。通过 preload + contextBridge 暴露需要的 API。

Q3: 如何在渲染进程中使用 Node.js 模块?

A: 不应该直接在渲染进程中用。正确做法:

  1. 在 preload 中 require 模块
  2. 用 contextBridge.exposeInMainWorld 暴露封装后的 API
  3. 或通过 IPC 转发给主进程执行

Q4: macOS 上关闭窗口后应用没退出

A: macOS 的惯例是关闭窗口不退出应用。如果你想要非 macOS 行为:

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
  // macOS: 保持应用活跃,等用户 Cmd+Q
});

Q5: 如何实现单实例锁定?

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', () => {
    // 有人试图启动第二个实例,聚焦到已有窗口
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
  });
}

Q6: 如何在 Electron 中使用本地图片?

A: 打包后的图片路径会变化。使用协议处理:

// 主进程注册自定义协议
protocol.registerFileProtocol('local', (request, callback) => {
  const filePath = request.url.replace('local://', '');
  callback({ path: path.normalize(filePath) });
});

// 渲染进程中使用
// <img src="local:///path/to/image.png">

Q7: 如何隐藏控制台窗口(Windows)?

// 在主进程入口处
if (process.platform === 'win32') {
  // electron-builder 配置中设置
  // win: { artifactName: '...', }
  // 或在 forge.config.js 中
}

Q8: 如何获取应用版本号?

const { app } = require('electron');
console.log(app.getVersion()); // 从 package.json 读取

Q9: 如何处理 Windows 的 SmartScreen 警告?

A: 需要代码签名。使用 EV 代码签名证书可以立即建立声誉,OV 证书需要一段时间。

Q10: Electron 支持移动端吗?

A: Electron 不支持移动端。如果需要移动端,考虑:

  • Tauri 2.0:支持 Android/iOS
  • Capacitor:Web 技术打包移动应用
  • React Native:原生移动应用

附录 C · 实战场景

场景 1:从零创建一个 Markdown 编辑器

# 1. 创建项目
npx @electron-forge/cli init markdown-editor
cd markdown-editor
npm install marked electron-store

# 2. 修改 src/main/index.js
// src/main/index.js
const { app, BrowserWindow, Menu, dialog } = require('electron');
const path = require('path');
const Store = require('electron-store');

const store = new Store({ defaults: { windowBounds: { width: 1000, height: 700 } } });
let win;

function createWindow() {
  const bounds = store.get('windowBounds');
  win = new BrowserWindow({
    ...bounds,
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,
    },
  });

  win.loadFile(path.join(__dirname, '../renderer/index.html'));
  win.on('close', () => store.set('windowBounds', win.getBounds()));
}

const menuTemplate = [
  {
    label: '文件',
    submenu: [
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: async () => {
          const { filePaths } = await dialog.showOpenDialog({
            filters: [{ name: 'Markdown', extensions: ['md', 'txt'] }],
          });
          if (filePaths[0]) {
            const content = require('fs').readFileSync(filePaths[0], 'utf-8');
            win.webContents.send('file:opened', content);
          }
        },
      },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        click: async () => {
          const { filePath } = await dialog.showSaveDialog({
            filters: [{ name: 'Markdown', extensions: ['md'] }],
          });
          if (filePath) {
            // 通过 IPC 获取渲染进程的内容
            const content = await win.webContents.executeJavaScript(
              'document.getElementById("editor").value'
            );
            require('fs').writeFileSync(filePath, content, 'utf-8');
          }
        },
      },
      { type: 'separator' },
      { role: 'quit' },
    ],
  },
];

app.whenReady().then(() => {
  Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
  createWindow();
});

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

场景 2:实现系统托盘 + 最小化到托盘

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

let tray;

function createTray(mainWindow) {
  const icon = nativeImage.createFromPath(path.join(__dirname, 'assets/tray.png'));
  icon.setTemplateImage(true);

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

  const menu = Menu.buildFromTemplate([
    { label: '显示', click: () => mainWindow.show() },
    { label: '隐藏', click: () => mainWindow.hide() },
    { type: 'separator' },
    { label: '退出', click: () => { tray.destroy(); app.quit(); } },
  ]);

  tray.setContextMenu(menu);
  tray.on('click', () => {
    if (mainWindow.isVisible()) mainWindow.hide();
    else mainWindow.show();
  });
}

// 最小化到托盘而非任务栏
mainWindow.on('minimize', (e) => {
  e.preventDefault();
  mainWindow.hide();
});

场景 3:实现自动更新完整流程

// src/main/updater.js
const { autoUpdater } = require('electron-updater');
const { BrowserWindow, dialog, Notification } = require('electron');

class Updater {
  constructor(mainWindow) {
    this.mainWindow = mainWindow;
    autoUpdater.autoDownload = false;
    autoUpdater.autoInstallOnAppQuit = true;
    this.setupListeners();
  }

  setupListeners() {
    autoUpdater.on('checking-for-update', () => {
      this.sendStatus('checking');
    });

    autoUpdater.on('update-available', (info) => {
      this.sendStatus('available', info);
      dialog.showMessageBox(this.mainWindow, {
        type: 'info',
        title: '发现新版本',
        message: `新版本 ${info.version} 可用`,
        detail: info.releaseNotes || '',
        buttons: ['立即下载', '稍后'],
      }).then((result) => {
        if (result.response === 0) autoUpdater.downloadUpdate();
      });
    });

    autoUpdater.on('update-not-available', () => {
      this.sendStatus('up-to-date');
    });

    autoUpdater.on('download-progress', (progress) => {
      this.sendStatus('downloading', {
        percent: progress.percent,
        speed: progress.bytesPerSecond,
      });
    });

    autoUpdater.on('update-downloaded', () => {
      this.sendStatus('downloaded');
      dialog.showMessageBox(this.mainWindow, {
        type: 'info',
        title: '更新就绪',
        message: '更新已下载,重启以安装',
        buttons: ['立即重启', '稍后'],
      }).then((result) => {
        if (result.response === 0) autoUpdater.quitAndInstall();
      });
    });

    autoUpdater.on('error', (err) => {
      this.sendStatus('error', err.message);
    });
  }

  sendStatus(status, data = {}) {
    this.mainWindow.webContents.send('update:status', { status, ...data });
  }

  check() {
    autoUpdater.checkForUpdates();
  }
}

module.exports = Updater;

场景 4:无边框窗口 + 自定义标题栏(类 VS Code)

// 主进程
const win = new BrowserWindow({
  frame: false,
  titleBarStyle: 'hiddenInset', // macOS: 保留交通灯
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
  },
});

// IPC 处理窗口控制
ipcMain.on('window:minimize', () => BrowserWindow.getFocusedWindow()?.minimize());
ipcMain.on('window:maximize', () => {
  const win = BrowserWindow.getFocusedWindow();
  if (win?.isMaximized()) win.unmaximize();
  else win?.maximize();
});
ipcMain.on('window:close', () => BrowserWindow.getFocusedWindow()?.close());
<!-- 渲染进程 -->
<div class="titlebar">
  <span class="title">My App</span>
  <div class="controls">
    <button onclick="window.api.minimize()">─</button>
    <button onclick="window.api.maximize()">□</button>
    <button onclick="window.api.close()">✕</button>
  </div>
</div>
.titlebar {
  -webkit-app-region: drag;
  height: 32px;
  background: #1e1e1e;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 8px;
}
.titlebar .controls button {
  -webkit-app-region: no-drag;
  background: none;
  border: none;
  color: #ccc;
  cursor: pointer;
  padding: 4px 12px;
}
.titlebar .controls button:hover {
  background: #333;
}

场景 5:多窗口 + 窗口间通信

// 主进程:窗口管理器
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;
  }

  sendToAll(channel, ...args) {
    this.windows.forEach((win) => {
      if (!win.isDestroyed()) win.webContents.send(channel, ...args);
    });
  }

  sendTo(name, channel, ...args) {
    const win = this.windows.get(name);
    if (win && !win.isDestroyed()) win.webContents.send(channel, ...args);
  }
}

const wm = new WindowManager();

// IPC 转发
ipcMain.on('window:send', (e, target, channel, ...args) => {
  wm.sendTo(target, channel, ...args);
});

总结:Electron 的核心是"用 Web 技术写桌面应用"。理解三个关键概念就够了:主进程 vs 渲染进程(职责分离)、IPC 通信(进程间桥梁)、安全模型(contextIsolation + preload + contextBridge)。剩下的都是 Web 开发的老本行。

本页目录