Electron 架构总览:主进程、渲染进程与进程间通信

Electron 经常被一句话概括成「用 Web 技术写桌面应用」,但真正落到工程上,如果只停留在这句话,很容易在安全、性能和架构上踩坑。
这一篇先不讲打包和配置,而是想搞清楚几件事:Electron 是怎么把 Chromium 和 Node 拼在一起的、主进程和渲染进程各自负责什么,以及两者之间的通信在工程上应该怎么设计。

从架构角度看,Electron 可以粗略拆成三块:

  • Chromium 渲染引擎
    • 提供浏览器级别的渲染能力(HTML/CSS/JS、DevTools、Web 标准等);
    • 每个窗口(BrowserWindow)背后都有一个对应的渲染进程。
  • Node.js 运行时
    • 提供文件系统、进程管理、网络等能力;
    • 在主进程和(可选的)渲染进程里提供 JS 侧的系统访问能力。
  • Electron 主进程
    • 管理应用生命周期(启动、退出、单实例等);
    • 创建和管理 BrowserWindow
    • 负责与操作系统集成(菜单、托盘、通知、协议、文件关联等)。

可以用一句话概括:

  • 主进程更像「系统服务 + 窗口管理器」,渲染进程更像「浏览器页面」,两者共享 Node 能力并通过 IPC 协作。

理解这一层,有助于后面在安全和职责划分上不把所有逻辑都堆进某一个进程。

在 Electron 应用里,主进程主要承担这些角色:

  • 应用生命周期管理:
    • 响应 app.on('ready')app.on('window-all-closed') 等事件;
    • 控制应用何时退出、单实例锁等。
  • 窗口与视图管理:
    • 创建 BrowserWindow,控制窗口尺寸、位置、样式;
    • 维护窗口列表、聚焦状态、多窗口通信等。
  • 与操作系统集成:
    • 菜单(Menu)、托盘(Tray)、通知(Notification)、Dock 图标;
    • 文件/URL 协议处理(通过 app.setAsDefaultProtocolClient 等)。
  • 封装本地能力:
    • 作为「本地 API 网关」,对外暴露受控接口给渲染进程使用。

在工程实践中,一个健康的模式是:

  • 主进程只做「需要系统权限」和「需要跨窗口协调」的事;
  • 不在主进程里堆业务 UI 逻辑,更不直接处理复杂渲染相关代码。

每个 BrowserWindow 底下,有一个对应的 WebContents

  • BrowserWindow 负责窗口本身的行为(大小、位置、最大化/最小化等);
  • WebContents 负责加载和运行具体的页面(URL 或本地 HTML)。

主进程通过这些对象可以:

  • 给窗口发送消息;
  • 监听页面加载、导航、崩溃等事件;
  • 在需要时对某些行为做拦截或限制。

渲染进程本质上就是:

  • 在 Chromium 里跑的一个「Web 应用」,负责:
    • UI 渲染(React/Vue/Svelte/原生 DOM);
    • 用户交互与状态管理;
    • 调用受限的本地 API(通过 IPC 或预暴露接口)。

在安全配置合适(例如 nodeIntegration: false, contextIsolation: true)的前提下,渲染进程应该尽量:

  • 像普通 Web 前端一样工作;
  • 不直接拥有全面的 Node 能力,而是通过主进程接口访问系统资源。

每个 BrowserWindow 默认对应一个独立的渲染进程:

  • 多窗口应用 = 多个渲染进程;
  • 每个渲染进程之间不能直接访问彼此的内存,需要通过主进程或其他通道通信。

在工程层面:

  • 需要决定哪些状态放在每个窗口内部管理;
  • 哪些状态由主进程或外部后端服务来协调。

主进程和渲染进程不在同一个 JS 运行上下文中:

  • 不能直接调用对方的函数;
  • 不能直接读写对方的变量。

为了让两边协作者感又不破坏隔离,Electron 提供了 IPC 机制:

  • 主进程侧:ipcMain
  • 渲染进程侧:ipcRenderer(在安全模式下一般通过 preload 暴露封装后的接口)。

可以抽象成两种常见模式:

  • 事件通知
    • 渲染进程向主进程发送某个事件(例如「用户点击了 X」);
    • 主进程根据事件做响应(例如「打开某个系统窗口」)。
  • 请求-响应
    • 渲染进程发起一个请求(例如「读取某个配置文件」);
    • 主进程执行操作后返回结果。

工程实践上更推荐统一为「请求-响应风格」的调用接口,避免到处散落着难以追踪的事件名。

直接在渲染进程里开启 Node 能力(nodeIntegration: true)虽然方便,但引入的风险非常大:

  • 一旦页面存在 XSS 漏洞,就相当于把本地 Node 权限暴露给任意脚本;
  • 桌面应用退化成「带本地 Root 权限的浏览器标签页」。

更推荐的模式是:

  • BrowserWindow 配置中:
    • nodeIntegration: false
    • contextIsolation: true
    • 指定一个预加载脚本(preload)。
  • 在 preload 脚本里:
    • 使用 contextBridge.exposeInMainWorld,挂一个有限的 API 到渲染进程全局对象上;
    • 内部通过 ipcRenderer 与主进程通信。

这样可以做到:

  • 渲染进程只能通过预定义好的 API 调用本地能力;
  • 主进程可以在这些 API 中加入权限检查、参数验证、日志审计等逻辑。

从架构角度看,相当于:

  • 主进程 + preload 组成一个「本地服务层」,渲染进程只是前端客户端。

在设计 Electron 应用架构时,一个常见误区是把所有后端逻辑都塞进主进程或渲染进程。
更健康的分工通常是:

  • 后端服务
    • 处理核心业务逻辑、多用户协作、持久化数据等;
    • 与桌面应用解耦,方便以后扩展到 Web 或移动端。
  • Electron 主进程
    • 负责本地环境相关的增强(文件系统、原生菜单、托盘、协议处理);
    • 封装成一组「本地 API」,通过 IPC 暴露给渲染进程。
  • 渲染进程
    • 用前端技术栈实现 UI/交互;
    • 通过 HTTP/WebSocket 调用远端后端服务,通过本地 API 调用主进程能力。

这样可以兼顾:

  • 桌面版的本地体验;
  • 服务端的伸缩能力与多端复用;
  • 架构上的清晰边界。

这一篇可以压缩成几条关键结论:

  • Electron 把 Chromium 和 Node 拼在一起,但通过「主进程 / 渲染进程」划分了职责:前者偏系统与窗口管理,后者偏 UI 与交互;
  • 进程间通信通过 IPC 完成,推荐用 preload + contextBridge 暴露受控本地 API,而不是在渲染进程里直接开启 Node 能力;
  • 在更大的系统中,Electron 通常作为桌面壳存在,核心业务逻辑仍然由独立后端服务负责,桌面应用通过「远端后端 + 本地 API」两层能力组合完成工作。

后面如果继续展开 Electron 相关内容,可以在这个架构基础上分别深入安全模型、性能与资源管理、自动更新与配置管理,以及和 IDE / Web 工具的集成方式。