Electron

技术栈
前端框架
desktopchromiumnodejscross-platformnative

概览

Electron 技术栈概览

Electron 是由 GitHub 开源的桌面应用开发框架,使用 Web 技术(HTML/CSS/JS)构建跨平台桌面应用。它将 Chromium 和 Node.js 打包在一起,让前端开发者也能写出 macOS/Windows/Linux 原生应用。

Electron 是什么?

  • Chromium + Node.js 的桌面运行时
  • 主进程(Node.js)+ 渲染进程(Chromium)
  • IPC 通信机制连接前后端
  • 自动更新、原生菜单、系统托盘等原生能力

解决什么问题?

  • 前端开发者想写桌面应用
  • 一套代码跨三端(Win/Mac/Linux)
  • 毕设需要桌面端程序(管理软件、工具类)
  • VS Code、Slack、Discord 都是 Electron 做的

关键特性:

  • main process + renderer process 双进程架构
  • ipcMain / ipcRenderer 进程通信
  • BrowserWindow 窗口管理
  • 自动更新(electron-updater)
  • 打包工具:electron-builder / electron-forge

安装

环境准备

  • 操作系统:Windows 10+ / macOS 11+ / Linux(Ubuntu 20.04+)
  • Node.js:>= 18.x(推荐 LTS)
  • 包管理器:npm / yarn / pnpm
  • 额外依赖
    • Windows:无需额外依赖
    • macOS:Xcode Command Line Tools(xcode-select --install
    • Linux:build-essentiallibgtk-3-devlibnotify-dev 等(详见常见问题)

安装命令

快速开始(使用官方脚手架)

# 使用 Electron Forge(官方推荐)
npm init electron-app@latest my-app -- --template=vite

# 或使用 electron-builder
npm create electron-vite

手动安装到现有项目

mkdir my-electron-app &;& cd my-electron-app
npm init -y
npm install --save-dev electron

package.json 添加启动脚本:

{
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  }
}

最小 main.js

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

function createWindow() {
  const win = new BrowserWindow({ width: 800, height: 600 });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

常见安装问题

1. Electron 下载慢/失败

  • 原因:Electron 二进制文件托管在 GitHub,国内可能缓慢
  • 解决:设置镜像
    # npm
    npm config set electron_mirror https://npmmirror.com/mirrors/electron/
    # 或使用环境变量
    export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
    

2. Linux 缺少系统依赖

  • 现象error while loading shared libraries: libgtk-3.so.0
  • 解决(Ubuntu/Debian):
    sudo apt install libgtk-3-dev libnotify-dev libnss3 libxss1 \
      libasound2 libxtst6 xdg-utils
    

3. macOS "app is damaged" 签名错误

  • 解决:开发阶段无需签名,确保 package.json 中正确配置。如需分发,必须通过 Apple 公证。

4. 渲染进程无法使用 Node.js API

  • 原因:Electron 12+ 默认启用 contextIsolation
  • 解决:使用 preload.js 通过 contextBridge 暴露安全 API,而非禁用 contextIsolation

示例

Electron 桌面记事本

目标

用 Electron 创建跨平台桌面记事本应用,展示主进程/渲染进程通信、原生菜单和文件保存。

完整代码

1. 项目初始化

mkdir electron-notepad &;& cd electron-notepad
npm init -y
npm install electron --save-dev

2. package.json

{
  "name": "electron-notepad",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  }
}

3. main.js

const { app, BrowserWindow, Menu, dialog, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  mainWindow.loadFile('index.html');

  // 原生菜单
  const menuTemplate = [
    {
      label: '文件',
      submenu: [
        {
          label: '打开',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const { filePaths } = await dialog.showOpenDialog({ filters: [{ name: '文本文件', extensions: ['txt'] }] });
            if (filePaths[0]) {
              const content = fs.readFileSync(filePaths[0], 'utf-8');
              mainWindow.webContents.send('file-opened', content);
            }
          },
        },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => mainWindow.webContents.send('trigger-save'),
        },
        { type: 'separator' },
        { label: '退出', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() },
      ],
    },
    {
      label: '帮助',
      submenu: [
        { label: '关于', click: () => dialog.showMessageBox({ title: '关于', message: '桌面记事本 v1.0\n毕设参考项目' }) },
      ],
    },
  ];

  Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
}

// IPC: 保存文件
ipcMain.handle('save-file', async (event, content) => {
  const { filePath } = await dialog.showSaveDialog({ filters: [{ name: '文本文件', extensions: ['txt'] }] });
  if (filePath) {
    fs.writeFileSync(filePath, content, 'utf-8');
    return { success: true, path: filePath };
  }
  return { success: false };
});

app.whenReady().then(createWindow);
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });

4. preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
  onFileOpened: (callback) => ipcRenderer.on('file-opened', (_, content) => callback(content)),
  onTriggerSave: (callback) => ipcRenderer.on('trigger-save', () => callback()),
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
});

5. index.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>桌面记事本</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: system-ui; }
    .toolbar { display: flex; gap: 8px; padding: 10px; background: #f1f5f9; border-bottom: 1px solid #e2e8f0; }
    .toolbar button { padding: 6px 14px; border: 1px solid #cbd5e1; background: white; border-radius: 4px; cursor: pointer; }
    .toolbar button:hover { background: #e2e8f0; }
    .info { margin-left: auto; color: #64748b; font-size: 0.85em; display: flex; align-items: center; }
    textarea { width: 100%; height: calc(100vh - 48px); padding: 16px; font-size: 16px; border: none; resize: none; outline: none; font-family: 'Fira Code', monospace; line-height: 1.6; }
  </style>
</head>
<body>
  <div class="toolbar">
    <button onclick="newFile()">📄 新建</button>
    <button onclick="saveFile()">💾 保存</button>
    <span class="info" id="status"></span>
  </div>
  <textarea id="editor" placeholder="在此输入..." autofocus></textarea>

  <script>
    const editor = document.getElementById('editor');
    const status = document.getElementById('status');

    // 打开文件时加载内容
    window.electronAPI?.onFileOpened((content) => {
      editor.value = content;
      status.textContent = '已打开文件';
    });

    // 菜单保存触发
    window.electronAPI?.onTriggerSave(() => saveFile());

    async function saveFile() {
      const result = await window.electronAPI?.saveFile(editor.value);
      if (result?.success) {
        status.textContent = `已保存: ${result.path}`;
        setTimeout(() => status.textContent = '', 2000);
      }
    }

    function newFile() {
      if (editor.value && !confirm('未保存的内容将丢失,确认新建?')) return;
      editor.value = '';
      status.textContent = '';
    }

    // 字数统计
    editor.addEventListener('input', () => {
      document.title = `记事本 - ${editor.value.length} 字`;
    });

    // Ctrl+S 快捷键
    document.addEventListener('keydown', (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        saveFile();
      }
    });
  </script>
</body>
</html>

运行步骤

npm start

预期输出

  • 打开桌面窗口,显示文本编辑器
  • 菜单栏 → 文件 → 打开/保存(原生对话框)
  • Ctrl+S 保存文件到本地
  • 窗口标题显示实时字数

Electron Hello World — 桌面应用

目标

创建一个完整的 Electron 桌面应用,展示主进程与渲染进程的基本通信。

完整代码

项目结构:

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

package.json:

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

main.js(主进程):

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

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 900,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,  // 安全:渲染进程无法直接访问 Node
      nodeIntegration: false,
    },
  });

  mainWindow.loadFile('index.html');

  // 开发时可打开 DevTools
  if (process.env.NODE_ENV === 'development') {
    mainWindow.webContents.openDevTools();
  }
}

// ===== 处理渲染进程的 IPC 请求 =====
ipcMain.handle('get-app-version', () => {
  return app.getVersion();
});

ipcMain.handle('show-message', async (event, message) => {
  const { dialog } = require('electron');
  dialog.showMessageBox(mainWindow, {
    type: 'info',
    title: '来自主进程',
    message: message,
  });
  return '消息已显示';
});

app.whenReady().then(createWindow);

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

preload.js(预加载脚本):

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

// 安全地向渲染进程暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  showMessage: (msg) => ipcRenderer.invoke('show-message', msg),
  platform: process.platform,
});

index.html(渲染进程):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Electron Hello World</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, 'Microsoft YaHei', sans-serif;
      display: flex; flex-direction: column; align-items: center;
      justify-content: center; height: 100vh;
      background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
      color: #eee;
    }
    h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
    .subtitle { color: #aaa; margin-bottom: 2rem; }
    button {
      padding: 12px 28px; margin: 8px;
      border: none; border-radius: 8px;
      font-size: 1rem; cursor: pointer;
      background: #7c4dff; color: white;
      transition: transform 0.2s, background 0.2s;
    }
    button:hover { transform: translateY(-2px); background: #651fff; }
    #version-info { margin-top: 1.5rem; color: #8f8; font-size: 0.9rem; }
  </style>
</head>
<body>
  <h1>🚀 Electron App</h1>
  <p class="subtitle">跨平台桌面应用就这么简单</p>

  <button id="btn-version">查看版本</button>
  <button id="btn-message">显示消息框</button>

  <div id="version-info"></div>

  <script>
    // 通过 preload 暴露的 API 访问 Electron 功能
    document.getElementById('btn-version').addEventListener('click', async () => {
      const version = await window.electronAPI.getAppVersion();
      document.getElementById('version-info').textContent =
        `当前版本:${version} | 平台:${window.electronAPI.platform}`;
    });

    document.getElementById('btn-message').addEventListener('click', async () => {
      await window.electronAPI.showMessage('你好,来自渲染进程!');
    });
  </script>
</body>
</html>

运行步骤

npm install
npm start

预期输出

  • 出现 900x600 桌面窗口,带渐变背景
  • 点击"查看版本"按钮显示 Electron 版本号和当前平台
  • 点击"显示消息框"按钮弹出系统原生对话框

教程

Electron 入门教程:构建生产力工具

一、Electron 是什么?

Electron 将 Chromium 和 Node.js 整合到一个运行时中,让 Web 开发者能构建跨平台桌面应用。知名项目包括 VS Code、Slack、Figma、Notion、Discord 等都基于 Electron。

架构理解

┌─────────────────────────────────┐
│         Main Process            │
│  (Node.js, 系统级API, 窗口管理)   │
│         main.js                 │
├─────────────────────────────────┤
│      Renderer Process x N       │
│  (Chromium, HTML/CSS/JS, 不能   │
│   直接访问Node, 需preload桥接)    │
└─────────────────────────────────┘
  • 主进程:每个应用只有一个,负责管理窗口、系统托盘、菜单等
  • 渲染进程:每个窗口一个,运行网页内容,与主进程通过 IPC 通信

二、分步构建一个任务管理器

第一步:项目初始化

npm init -y
npm install electron --save-dev

第二步:主进程(窗口管理 + 系统托盘)

// main.js
const { app, BrowserWindow, Tray, Menu, ipcMain, Notification } = require('electron');
const path = require('path');

let mainWindow, tray;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
    },
    titleBarStyle: 'hiddenInset', // macOS 风格
  });
  mainWindow.loadFile('index.html');
}

// 系统托盘
function createTray() {
  tray = new Tray(path.join(__dirname, 'icon.png'));
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示窗口', click: () => mainWindow.show() },
    { label: '退出', click: () => app.quit() },
  ]);
  tray.setToolTip('我的任务管理器');
  tray.setContextMenu(contextMenu);
}

第三步:数据持久化(主进程)

const fs = require('fs').promises;
const DATA_PATH = path.join(app.getPath('userData'), 'tasks.json');

ipcMain.handle('load-tasks', async () => {
  try {
    const data = await fs.readFile(DATA_PATH, 'utf-8');
    return JSON.parse(data);
  } catch {
    return []; // 文件不存在时返回空数组
  }
});

ipcMain.handle('save-tasks', async (event, tasks) => {
  await fs.writeFile(DATA_PATH, JSON.stringify(tasks, null, 2));
  return true;
});

第四步:Preload 安全桥

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

contextBridge.exposeInMainWorld('app', {
  loadTasks: () => ipcRenderer.invoke('load-tasks'),
  saveTasks: (tasks) => ipcRenderer.invoke('save-tasks', tasks),
  onReminder: (callback) => ipcRenderer.on('reminder', callback),
});

第五步:渲染进程 UI

<!-- index.html 核心结构 -->
<div id="app">
  <input id="task-input" placeholder="添加新任务..." />
  <button id="add-btn">添加</button>
  <ul id="task-list"></ul>
</div>
<script src="renderer.js"></script>

第六步:打包分发

使用 Electron Forge:

npm install --save-dev @electron-forge/cli
npx electron-forge import
npm run make  # 生成 .exe / .dmg / .deb

三、常见陷阱与最佳实践

  1. 永远不要禁用 contextIsolation——这是最重要的安全防线
  2. 大文件操作用 Worker 线程,避免阻塞主进程
  3. 自动更新使用 electron-updater 实现
  4. 内存泄漏:每个窗口都是独立进程,及时释放关闭窗口的引用
  5. IPC 通信数据需序列化,不能传递函数或复杂对象引用

四、思考题

  1. 如何在不关闭窗口的情况下隐藏到系统托盘?
  2. app.getPath('userData') 在不同操作系统上分别对应什么目录?
  3. 为什么 Electron 不适合做 CPU 密集型后端服务?

Electron 桌面应用开发入门

背景

Electron 让前端开发者能用 HTML/CSS/JS 写桌面应用。VS Code、Slack、Figma、Discord 都是 Electron 做的。如果你的毕设需要一个桌面端程序(管理系统、工具软件、数据可视化客户端),Electron 是唯一用前端技术栈就能搞定的方案。


核心概念

双进程架构

┌─────────────────────────┐
│     Main Process        │  ← Node.js 环境
│  (main.js)              │
│  - 窗口管理              │  - 文件系统 fs
│  - 原生菜单              │  - 系统通知
│  - IPC 通信              │  - 自动更新
│                         │
│  ←── ipcMain / ipcRenderer ──→ │
│                         │
│     Renderer Process    │  ← Chromium 环境
│  (index.html + JS)      │
│  - 页面渲染              │  - Canvas/WebGL
│  - 用户交互              │  - 任意前端框架
└─────────────────────────┘

IPC 通信

// Main Process
ipcMain.handle('get-data', async () => {
  return await db.query('SELECT * FROM users');
});

// Renderer Process (preload)
contextBridge.exposeInMainWorld('api', {
  getData: () => ipcRenderer.invoke('get-data'),
});

// Renderer (页面)
const data = await window.api.getData();

分步操作

第一步:创建项目

npm init electron-app@latest my-app -- --template=vite
cd my-app
npm start

第二步:理解项目结构

my-app/
├── package.json
├── forge.config.js     # Electron Forge 配置
├── src/
│   ├── main.js          # 主进程
│   ├── preload.js       # 预加载脚本(安全桥接)
│   ├── index.html       # 渲染进程入口
│   └── renderer.js      # 渲染进程 JS

第三步:创建窗口

// main.js
const { app, BrowserWindow } = require('electron');

function createWindow() {
  const win = new BrowserWindow({
    width: 1024,
    height: 768,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,  // 安全:隔离渲染进程
    },
  });

  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

第四步:加原生菜单

const { Menu } = require('electron');
const menu = Menu.buildFromTemplate([
  {
    label: '文件',
    submenu: [
      { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => { /* ... */ } },
      { label: '退出', role: 'quit' },
    ],
  },
]);
Menu.setApplicationMenu(menu);

毕设推荐场景

桌面管理系统

学生管理、图书管理、库存管理——Electron + Vue/React + SQLite。

串口/硬件工具

通过 Node.js 的 serialport 库连接 Arduino/传感器。

数据可视化客户端

Electron + ECharts/D3.js,比网页版更强(本地文件读写)。


思考题

  1. 为什么 Electron 需要 contextIsolation 和 preload 脚本?
  2. Main Process 和 Renderer Process 各能访问哪些 API?
  3. Electron 打包后的体积通常很大(100MB+),如何优化?

小结

Electron 让前端技术栈覆盖桌面端。毕设用它写一个桌面应用,不仅实用性强,答辩时也能展示跨平台开发能力。