Electron (原名 Atom Shell) 是一个由 GitHub 开发的开源框架,它允许你使用 Web 技术 (HTML, CSS, JavaScript) 来构建跨平台的桌面应用程序。通过将 Chromium 渲染引擎和 Node.js 运行时集成到一个单一的项目中,Electron 使得前端开发者只需掌握一套技术栈,就能创建出功能强大、拥有原生外观和感觉的桌面应用,并能够访问操作系统底层功能。

核心思想:借助 Chromium (渲染视图) 和 Node.js (处理操作系统交互) 的能力,Electron 让 Web 技术栈能够构建全功能的跨平台桌面应用。


一、Electron 简介与优势

1.1 什么是 Electron?

Electron 可以被简单理解为一个微型浏览器套壳,它包含了:

  • Chromium:提供渲染用户界面的能力,这意味着你可以使用 HTML、CSS 和 JavaScript 来构建应用的 UI。
  • Node.js:提供访问操作系统底层 API 的能力,例如文件系统、网络、进程管理等。
  • 原生 API 集成:Electron 提供了一套 API,用于访问操作系统特定的功能,如菜单、通知、窗口管理等。

1.2 Electron 的优势

  • 跨平台:只需编写一次代码,即可在 Windows、macOS 和 Linux 上运行。
  • Web 技术栈:前端开发者可以利用熟悉的 HTML、CSS 和 JavaScript 技能快速上手。
  • 丰富的生态系统:受益于庞大的 Web 和 Node.js 生态系统,有无数的库和工具可用。
  • 强大的功能:Node.js 赋予了应用强大的后端能力与系统级交互。
  • 社区活跃:拥有庞大且活跃的开发者社区和 GitHub 的持续支持。

1.3 知名应用

许多知名的桌面应用都是基于 Electron 构建的,例如:

  • VS Code
  • Slack
  • Discord
  • Skype (新版)
  • GitHub Desktop
  • Notion

二、Electron 架构:主进程与渲染进程

Electron 应用的核心是一个多进程架构,类似于 Web 浏览器:

2.1 主进程 (Main Process)

  • 角色:负责管理应用程序的生命周期、创建和管理窗口、处理系统事件、与原生操作系统 API 交互。
  • 环境:运行在一个 Node.js 环境中,因此拥有 Node.js 的所有功能。
  • 文件:通常只有一个主进程,其入口文件通常是 main.js 或类似名称。
  • 权限:拥有完全访问操作系统底层资源的权限。
  • 实例BrowserWindow 实例 (new BrowserWindow()),用于创建新的渲染进程窗口。

2.2 渲染进程 (Renderer Process)

  • 角色:负责渲染每个窗口中的用户界面。每个 BrowserWindow 实例都运行在一个独立的渲染进程中。
  • 环境:运行在一个 Chromium 环境中,与普通 Web 页面类似,拥有 DOM、BOM 和 Web API。
  • 权限:默认情况下,出于安全考虑,渲染进程无法直接访问 Node.js API (可以通过 contextBridgenodeIntegration 改变)。

2.3 进程间通信 (IPC - Inter-Process Communication)

由于主进程和渲染进程运行在不同的线程中,它们需要通过 IPC 机制进行通信。

  • ipcMain:主进程模块,监听来自渲染进程的消息,并向渲染进程发送消息。
  • ipcRenderer:渲染进程模块,向主进程发送消息,并监听来自主进程的消息。

通信示例:

主进程 (main.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

let mainWindow;

app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 预加载脚本
// nodeIntegration: true, // ⚠️ 不推荐:允许渲染进程直接访问 Node.js API
// contextIsolation: false, // ⚠️ 不推荐:关闭上下文隔离
},
});

mainWindow.loadFile('index.html');

// 监听来自渲染进程的消息
ipcMain.on('send-message-to-main', (event, arg) => {
console.log(`Received from renderer: ${arg}`); // prints "ping"
event.reply('reply-from-main', 'pong'); // 回复给渲染进程
});
});

预加载脚本 (preload.js)

预加载脚本在渲染进程内容加载之前运行,并可以安全地暴露 Node.js API 给渲染进程的上下文。

1
2
3
4
5
6
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
sendMessageToMain: (message) => ipcRenderer.send('send-message-to-main', message),
onReplyFromMain: (callback) => ipcRenderer.on('reply-from-main', (event, arg) => callback(arg)),
});

渲染进程 (renderer.js 或直接写在 index.html 中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>Electron App</title>
</head>
<body>
<h1>Hello Electron!</h1>
<button id="send-btn">Send Message to Main</button>
<p>Main Process Reply: <span id="reply-text"></span></p>

<script>
// 通过 contextBridge 暴露的 API 访问
document.getElementById('send-btn').addEventListener('click', () => {
window.electronAPI.sendMessageToMain('ping');
});

window.electronAPI.onReplyFromMain((message) => {
document.getElementById('reply-text').innerText = message;
});
</script>
</body>
</html>

三、Electron 开发环境搭建与 Hello World

3.1 目录结构推荐

1
2
3
4
5
6
7
8
9
.
├── package.json # 项目配置,包含 Electron 启动脚本
├── main.js # 主进程入口文件
├── index.html # 渲染进程 UI 入口
├── preload.js # 在渲染进程中预加载的脚本
├── renderer.js # 渲染进程的 JavaScript (如果复杂,单独文件)
├── assets/ # 静态资源,如图标
├── node_modules/ # npm 依赖
└── .gitignore

3.2 最小化 Hello World 示例

  1. 创建项目文件夹并初始化 npm

    1
    2
    3
    mkdir my-electron-app
    cd my-electron-app
    npm init -y
  2. 安装 Electron:

    1
    npm install --save-dev electron
  3. 编辑 package.json
    添加一个启动脚本,指向主进程文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "name": "my-electron-app",
    "version": "1.0.0",
    "description": "A minimal Electron application",
    "main": "main.js", # 指向主进程文件
    "scripts": {
    "start": "electron .", # 启动 Electron 应用
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "Your Name",
    "license": "MIT",
    "devDependencies": {
    "electron": "^最新版本" # 例如 ^28.0.0
    }
    }
  4. 创建 main.js (主进程文件):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    const { app, BrowserWindow } = require('electron')
    const path = require('path')

    function createWindow () {
    const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
    preload: path.join(__dirname, 'preload.js') // 使用预加载脚本增强安全性
    }
    })

    mainWindow.loadFile('index.html')

    // 可选:打开开发者工具
    // mainWindow.webContents.openDevTools()
    }

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

    app.on('activate', function () {
    // 在 macOS 上,当所有窗口都关闭后,通常在点击 Dock 图标时重新创建一个窗口。
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
    })

    app.on('window-all-closed', function () {
    if (process.platform !== 'darwin') app.quit()
    })
  5. 创建 index.html (渲染进程 UI 文件):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Hello Electron!</title>
    <link rel="stylesheet" href="index.css">
    </head>
    <body>
    <h1>Hello from Electron!</h1>
    <p>Current Node.js version: <span id="node-version"></span></p>
    <p>Current Chromium version: <span id="chrome-version"></span></p>
    <p>Current Electron version: <span id="electron-version"></span></p>
    <script src="renderer.js"></script>
    </body>
    </html>
  6. 创建 preload.js (预加载脚本):

    1
    2
    3
    4
    5
    6
    7
    8
    const { contextBridge } = require('electron')

    contextBridge.exposeInMainWorld('versions', {
    node: () => process.versions.node,
    chrome: () => process.versions.chrome,
    electron: () => process.versions.electron
    // 你也可以在这里暴露其他 IPC 通信方法
    })
  7. 创建 renderer.js (渲染进程脚本):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // contextBridge 暴露的对象在 window.versions 上
    const information = document.getElementById('info')
    if (information) { // 检查元素是否存在
    information.innerText = `This app is using Node.js ${versions.node()},
    Chromium ${versions.chrome()}, and Electron ${versions.electron()}.`
    }

    const setVersionText = (id, text) => {
    const element = document.getElementById(id);
    if (element) {
    element.innerText = text;
    }
    };

    setVersionText('node-version', window.versions.node());
    setVersionText('chrome-version', window.versions.chrome());
    setVersionText('electron-version', window.versions.electron());
  8. 创建 index.css (可选,渲染进程样式文件):

    1
    2
    3
    4
    5
    6
    body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    margin: auto;
    max-width: 38rem;
    padding: 2rem;
    }
  9. 运行应用:

    1
    npm start

    你将看到一个显示 “Hello from Electron!” 和版本信息的桌面窗口。

四、安全最佳实践

由于 Electron 应用同时拥有 Web 内容和对操作系统的访问权限,安全性至关重要。

  • 启用 contextIsolation:默认开启。防止预加载脚本中的 Node.js 环境与渲染进程的 JavaScript 环境混淆。
  • 使用 preload.jscontextBridge:通过 contextBridge 暴露受限的 API 给渲染进程,避免直接在渲染进程中启用 nodeIntegration
  • 禁用 nodeIntegration:默认关闭。防止在渲染进程中直接访问 Node.js API。
  • 禁用 webview 中的 nodeIntegration:如果使用 webview 标签加载外部内容,务必禁用其 nodeIntegration
  • 限制导航:使用 webContents.on('will-navigate')webContents.on('new-window') 阻止或限制应用导航到非信任的 URL。
  • 处理内容沙箱:Chromium 的沙箱机制有助于隔离不信任的内容。
  • 内容安全策略 (CSP):在 index.html 中设置 CSP 来限制允许加载的资源。
  • 避免加载远程内容:优先加载本地文件 (file://),避免加载远程 URL (http://, https://),除非你完全信任这些内容。

五、打包和分发

完成开发后,你需要将 Electron 应用程序打包成可分发的安装包。常用的工具有:

  • electron-builder:功能强大、配置灵活,支持多种平台和输出格式 (exe, dmg, deb, snap 等)。
  • electron-packager:相对简单,只负责将应用打包成可执行文件,不创建安装包。

使用 electron-builder 示例:

  1. 安装 electron-builder

    1
    npm install --save-dev electron-builder
  2. package.json 中添加配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    {
    "name": "my-electron-app",
    "version": "1.0.0",
    "description": "...",
    "main": "main.js",
    "scripts": {
    "start": "electron .",
    "pack": "electron-builder --dir", # 只打包不创建安装程序
    "dist": "electron-builder" # 生成安装程序
    },
    "build": {
    "appId": "com.yourcompany.yourapp",
    "productName": "MyElectronApp",
    "copyright": "Copyright © 2025 ${author}",
    "directories": {
    "output": "dist"
    },
    "files": [
    "**/*",
    "!node_modules/*/{electron,electron-builder,electron-packager}*" # 排除开发依赖
    ],
    "win": {
    "target": ["nsis"],
    "icon": "build/icon.ico"
    },
    "mac": {
    "category": "public.app-category.utilities",
    "target": ["dmg"],
    "icon": "build/icon.icns"
    },
    "linux": {
    "target": ["AppImage"],
    "icon": "build/icons"
    }
    },
    "devDependencies": {
    "electron": "^最新版本",
    "electron-builder": "^最新版本"
    }
    }
  3. 运行打包命令:

    1
    npm run dist

    这将在 ./dist 目录下生成针对不同操作系统的安装包。

六、集成前端框架

Electron 应用的渲染进程本质上就是一个 Web 页面,因此你可以自由地集成任何前端框架,如 React、Vue、Angular 等。

  • 通常做法:使用 Create React App、Vue CLI、Angular CLI 等工具创建一个前端项目,将其构建输出 (如 builddist 目录) 作为 Electron 渲染进程的启动内容。

开发流程:

  1. 创建前端项目create-react-app my-react-app
  2. 构建前端项目npm run build (通常会生成 build 目录)
  3. 配置 Electron 加载:在 main.js 中,将 mainWindow.loadFile('index.html') 修改为 mainWindow.loadFile(path.join(__dirname, 'build', 'index.html'))
  4. 开发体验优化: 在开发模式下,可以配置 Electron 加载前端开发服务器的 URL (如 localhost:3000),实现热重载。
    1
    2
    3
    4
    5
    6
    // main.js (开发模式)
    if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:3000'); // React Dev Server
    } else {
    mainWindow.loadFile(path.join(__dirname, 'build', 'index.html'));
    }
    同时需要确保前端项目和 Electron 项目可以并行启动。

七、总结

Electron 为 Web 开发者打开了桌面应用开发的大门。它通过结合 Chromium 的强大渲染能力和 Node.js 的系统级访问能力,提供了一个灵活、高效的跨平台解决方案。理解主进程与渲染进程的架构、IPC 通信机制以及安全最佳实践是开发高质量 Electron 应用的关键。通过合理的项目结构、前端框架集成和高效的打包分发工具,你可以将你的 Web 应用带到桌面,提供更加无缝和强大的用户体验。