这份教程专为资深前端开发设计,通过图解和类比,帮助你从 Web 开发思维切换到 Electron 桌面端架构,彻底理解主进程、预加载脚本和渲染进程。
1. 核心概念与架构模型
理解 Electron 最快的方式,是将其视为一个微型 C/S(客户端/服务端)架构。你平时写的 Web 前端应用变成了“客户端”,而主进程变成了“Node.js 服务器”,预加载脚本则是“API 网关”。
1.1 架构模型图
graph TD
subgraph OS_Layer [操作系统底层]
OS[文件系统, 原生窗口, 硬件接口]
end
subgraph Node_js [Node.js 完整环境]
Main["主进程 (Main Process)<br/>入口: src/main/index.ts<br/>角色: '本地后端'"]
end
subgraph Chromium [Chromium 浏览器引擎]
subgraph V8_1 [V8 隔离上下文 1: 受限沙箱]
Renderer["渲染进程 (Renderer Process)<br/>入口: src/renderer/...<br/>角色: '纯净前端' (Vue/React)"]
end
subgraph V8_2 [V8 隔离上下文 2: 特权桥梁]
Preload["预加载脚本 (Preload Script)<br/>入口: src/preload/index.ts<br/>角色: 'API网关/服务员'"]
end
end
%% 交互关系
Main -->|完全控制| OS
Main <-->|IPC 通道| Preload
Preload -->|contextBridge.exposeInMainWorld| Renderer
Renderer -.->|禁止直接访问 X| OS
Renderer -.->|禁止直接访问 X| Main
classDef mainNode fill:#f9f0ff,stroke:#9c27b0,stroke-width:2px,color:#333333;
classDef preloadNode fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#333333;
classDef rendererNode fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#333333;
classDef osNode fill:#f5f5f5,stroke:#616161,stroke-width:2px,color:#333333;
class Main mainNode;
class Preload preloadNode;
class Renderer rendererNode;
class OS osNode;
1.2 为什么这样设计?(安全隔离 Context Isolation)
早期的 Electron 允许渲染进程直接使用 require('fs'),这造成了巨大的安全灾难。如果前端代码中引入的第三方包或 CDN 脚本被注入恶意代码(如 XSS 攻击),它们就能直接操作用户的本地文件系统。
因此,现代 Electron 采用了严格的上下文隔离(Context Isolation):
- 渲染进程 (Renderer):被彻底关进了 Chromium 的沙箱里。这里只有单纯的浏览器环境(DOM、BOM),绝对禁止接触 Node.js。XSS 攻击也只能在浏览器环境中生效。
- 主进程 (Main):拥有 Node.js 的完全权限,可以操作系统底层。
- 预加载脚本 (Preload):这是一个拥有特权的神奇地带。它在渲染进程启动前加载,运行在 Chromium 中,但有权访问 Node.js 的部分 API(如 ipcRenderer)。
1.3 通俗比喻:餐厅模型
- 主进程 = 后厨:掌管所有食材(系统文件、硬件)。
- 渲染进程 = 大堂顾客:只负责看菜单、点菜、吃饭(UI 渲染)。
- 预加载脚本 = 服务员:顾客(前端)不能直接冲进后厨(Node.js),只能通过服务员(Preload)点菜。服务员有一份固定的“菜单”(
contextBridge.exposeInMainWorld),上面只暴露出允许点的菜。
2. API 边界:谁能用什么?
因为职责和沙箱的限制,它们各自拥有的 API 是严格区分的:
| 进程 | 角色类比 | 核心可用 API 举例 | 绝对禁止使用的 API |
|---|---|---|---|
| 主进程 (Main) | Node.js 服务器 | app (应用生命周期)BrowserWindow (原生窗口操作)ipcMain (收听请求)fs, path, sqlite3 等所有 Node 原生模块 | window, documentlocalStorageDOM 操作 (因为没有界面) |
| 预加载脚本 (Preload) | API 网关 / SDK | contextBridge (挂载安全 API 到前端)ipcRenderer (负责与主进程通讯) | 虽然能用部分 Node API,但强烈建议只做事件转发,不写业务逻辑 |
| 渲染进程 (Renderer) | 纯粹的前端应用 | window.document (DOM API)fetch, localStorageVue, React 等框架代码预加载脚本挂载的方法 (如 window.api.xxx) | require, fs, pathipcRenderer (已禁止前端直接用)所有 Node.js 模块 |
3. IPC 通信流程与实战代码
我们来看一个最经典的场景:前端(Vue)想读取用户电脑上的一个本地文件。它是如何通过这三层架构跑通的?
3.1 时序图:前端如何读写本地文件?
sequenceDiagram
participant Vue as 渲染进程 (Vue 组件)<br/>window.api.readFile()
participant Preload as 预加载脚本 (Preload)<br/>contextBridge + ipcRenderer
participant Main as 主进程 (Main)<br/>ipcMain + fs
Note over Vue,Main: 目标:Vue 想读取 C:\config.txt 文件
Vue->>Preload: 1. 调用暴露的方法 <br/> await window.api.readFile('C:\\config.txt')
Note right of Preload: 这是安全网关,检查格式是否合法
Preload->>Main: 2. 通过 IPC 通道发送请求 <br/> ipcRenderer.invoke('read-file', path)
Note right of Main: 主进程拥有最高权限<br/>执行耗时的 Node.js 原生操作
Main->>Main: 3. 调用 Node.js API: <br/> fs.readFileSync(path)
Main-->>Preload: 4. 返回文件内容字符串 (或报错)
Preload-->>Vue: 5. Promise resolve,Vue 拿到数据
Note over Vue: 6. Vue 将内容渲染到 DOM 页面上
3.2 模式 A:前端请求并等待结果 (最常用)
适用于获取数据、读文件等异步操作,类似 Axios 的 GET 请求。
第一步:后厨准备做菜 (主进程 src/main/index.ts)
负责提供接口。
import { ipcMain } from 'electron'
import fs from 'node:fs'
// 注册名为 'read-file' 的服务
ipcMain.handle('read-file', async (event, filePath) => {
// 这里可以做安全校验
return fs.readFileSync(filePath, 'utf-8')
})
第二步:服务员准备菜单 (预加载脚本 src/preload/index.ts)
暴露给前端。
import { contextBridge, ipcRenderer } from 'electron'
// 向前端 window 暴露一个只读的、受限的 api 对象
contextBridge.exposeInMainWorld('api', {
// 封装底层的 ipc 通信,对外只暴露普通函数
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
})
第三步:顾客点菜 (渲染进程 Vue 组件 src/renderer/src/App.vue)
<script setup lang="ts">
import { ref } from 'vue'
const content = ref('')
async function loadConfig() {
// 前端完全感觉不到底层的 IPC 和 Node.js
// 就像调用一个普通的异步 SDK 函数一样
content.value = await window.api.readFile('C:\\config.txt')
}
</script>
3.3 模式 B:前端单向发送消息
适用于通知主进程执行某个动作,不需要回调。
主进程:
ipcMain.on('log', (event, msg) => console.log(msg))
前端:
window.electron.ipcRenderer.send('log', 'hello')
3.4 模式 C:主进程主动推消息
适用于下载进度、后台系统通知等,类似 WebSocket 接收。
主进程:
// 主动向某个窗口的前端推送数据
mainWindow.webContents.send('download-progress', 50)
前端:
window.electron.ipcRenderer.on('download-progress', (event, value) => {
console.log(`进度: ${value}%`) // 输出 50%
})
4. Electron 特有开发注意事项
4.1 跨域问题
在 Electron 渲染进程(Vue)中发起 Ajax 请求,依然受浏览器同源策略限制。
- 推荐方案: 在主进程中使用 Node.js 的
axios或原生fetch发起请求,通过 IPC 返回给前端。Node.js 环境没有跨域概念。 - 临时方案: 参考项目
src/main/index.ts中注释掉的onHeadersReceived代码,通过拦截并修改响应头(移除 CSP,修改 Cookie)来绕过跨域限制。
4.2 原生模块访问
绝对不要在 Vue 组件里直接 import fs from 'fs'。
- 所有涉及文件系统、注册表、系统硬件的操作,必须写在
src/main/目录中。 - 写好后,通过
src/preload/暴露一个触发接口给前端。
4.3 调试技巧
- 渲染进程 (Vue): 在开发环境下,点击窗口按
F12或Ctrl+Shift+I就能打开熟悉的 Chrome DevTools。 - 主进程 (Node): 在 VS Code 中配置调试器,或者直接查看运行
pnpm dev的终端面板里输出的日志。
5. 常用开发场景速查
5.1 打开外部链接
不要让 Electron 窗口内部跳转到外部网站,应唤起系统默认浏览器:
// 主进程中
import { shell } from 'electron'
shell.openExternal('https://google.com')
(注:如果主进程已配置全局拦截,前端直接 <a> 标签新开网页会自动用外部浏览器打开)
5.2 窗口操作(最小化、关闭)
前端由于没有权限,需要通知主进程来操作:
// 前端
window.api.closeWindow()
// 主进程响应
ipcMain.on('close-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.close()
})
5.3 路径处理
始终使用 Node 的 path.join 处理路径,避免 Windows (\) 和 macOS (/) 的斜杠差异。
import path from 'node:path'
import { app } from 'electron'
// 获取用户本地的数据存储目录(各系统自动适配)
const dbPath = path.join(app.getPath('userData'), 'local.db')
6. 核心 API 速查与三层关联映射
为了方便快速开发,下表总结了功能在三层架构中的流动方式以及各层最常用的 API。
6.1 功能流转模式 (Renderer -> Preload -> Main)
| 交互模式 | 渲染进程 (Renderer) | 预加载脚本 (Preload) | 主进程 (Main) |
|---|---|---|---|
| 异步请求/响应 (最常用) | window.api.xxx() | ipcRenderer.invoke('channel') | ipcMain.handle('channel', ...) |
| 前端单向通知主进程 | window.api.xxx() | ipcRenderer.send('channel') | ipcMain.on('channel', ...) |
| 主进程主动推送前端 | window.api.onXxx(callback) | ipcRenderer.on('channel') | mainWindow.webContents.send('channel') |
6.2 各层常用 API 速查表
| 层次 | 核心 API | 主要用途 |
| :--- | :--- | :--- | :--- |
| 主进程 (Main) | app | 管理应用的生命周期(就绪、退出等) |
| | BrowserWindow | 创建和控制浏览器窗口 |
| | ipcMain | 处理来自渲染进程的同步或异步通信 |
| | dialog | 显示原生系统对话框(打开文件、保存、警告) |
| | shell | 使用默认应用程序管理文件和 URL(如打开外部浏览器) |
| | Menu / Tray | 创建原生应用菜单和系统托盘图标 |
| | nativeTheme | 读取并响应系统主题(深色/浅色)的变化 |
| 预加载 (Preload) | contextBridge.exposeInMainWorld | 安全地将 API 从预加载脚本暴露给渲染进程 |
| | ipcRenderer | 在预加载脚本中与主进程进行通信 |
| 渲染进程 (Renderer) | window.api | 开发者在 Preload 中定义的自定义对象,前端唯一合法的通信入口 |
6.3 架构核心原则:Preload 是“哑管道”
在开发过程中,请务必记住:Preload 脚本应该只是一个“哑管道”(Dumb Pipe)。
- 不要在 Preload 中编写复杂的业务逻辑。
- 不要在 Preload 中直接进行文件处理或数据库操作。
- 只做转发:将前端的函数调用转发给
ipcRenderer,或将主进程的事件转发给前端的回调函数。
保持 Preload 简洁,可以有效降低安全风险并使架构层次更加清晰。