Electron 架构反模式与重构建议:从单文件主进程到分层本地服务
很多 Electron 项目的起点都是一个「能跑起来的 Demo」,但如果在这个基础上一路堆功能,很容易发展成一个「几千行的主进程大杂烩」和「到处直接用 Node 的渲染进程」。
这一篇尝试把常见的 Electron 架构反模式梳理出来,并给出对应的重构方向:如何从单文件主进程走向分层本地服务,如何收紧渲染进程的权限,以及如何整理 IPC 和状态管理。
1. 反模式一:所有逻辑都堆在主进程里
1.1 症状描述
常见表现包括:
main.js或main.ts文件动辄几千行:- 创建窗口;
- 菜单/托盘定义;
- 所有 IPC 处理;
- 文件读写、网络请求、业务逻辑混杂在一起。
- 新需求一来,习惯性地「继续往主进程里加」:
- 在某个 IPC handler 里直接写业务;
- 主进程开始负责各种「不应该由它负责」的细节。
结果是:
- 主进程难以测试,也难以复用;
- 一旦发生问题,很难快速定位到底是哪一块出错。
1.2 重构方向
可以用类似服务分层的思路重构主进程:
- 把主进程拆成几个模块:
- 窗口管理模块:只负责
BrowserWindow的创建/销毁/聚焦; - 系统集成模块:菜单、托盘、快捷键、通知;
- 本地服务模块:文件系统、配置管理、任务执行等;
- 窗口管理模块:只负责
- IPC handler 不直接写业务逻辑,而是简单调用对应服务模块的方法。
从结构上看,主进程更像是:
- 一个「本地服务容器」+ 「窗口管理器」,而不是「所有逻辑的垃圾桶」。
2. 反模式二:渲染进程直接开启 Node 能力
2.1 症状描述
典型配置:
- 所有
BrowserWindow都设置nodeIntegration: true; - 渲染进程代码中广泛使用:
1const fs = require("fs");
2const { exec } = require("child_process");
甚至:
- 把数据库访问、系统脚本、内部 API 调用都直接写在渲染进程里。
这会带来几个问题:
- 安全风险显著增大(XSS → 本地代码执行);
- 渲染代码难以移植到 Web 环境;
- 业务逻辑和本地能力强耦合,测试困难。
2.2 重构方向
收紧渲染进程权限可以分两步走:
- 在配置上:
- 把
nodeIntegration关掉; - 开启
contextIsolation,引入 preload 脚本。
- 把
- 在代码结构上:
- 把所有直接使用 Node 的代码迁移到主进程或专门的本地服务模块;
- 在 preload 里为这些服务暴露受控 API(通过
contextBridge),渲染进程只调用这些 API。
最终结构更接近:
- 渲染进程:前端应用,只调用
window.xxxApi; - 主进程 + 本地服务模块:实现这些 API 背后的实际系统操作。
3. 反模式三:IPC 名字和协议散落各处
3.1 症状描述
在项目早期,IPC 常常是临时起名、临时使用,后来就变成:
- 到处可见类似字符串:
1ipcMain.handle("get-config", ...)
2ipcRenderer.invoke("get-config")
3
4ipcMain.on("task-started", ...)
5ipcRenderer.send("task-started", ...)
- 没有集中定义,难以追踪调用关系;
- 没有统一的错误处理和超时策略。
随着功能增多,IPC 层就会像一个「任意门」:
谁想加一个通道,就随手起个名字发消息。
3.2 重构方向
可以把 IPC 当成一种「本地 RPC 协议」来整理:
- 集中定义 IPC 通道和 payload 结构:
- 抽出一个常量/类型定义文件,定义所有 channel 名和参数/返回类型;
- 避免在代码中散写字符串。
- 封装一层「客户端 SDK」:
- 在 preload 中为每类操作提供一个函数,例如
configApi.read()、taskApi.start(); - 内部统一使用
ipcRenderer.invoke/ipcRenderer.send,并统一处理错误/超时。
- 在 preload 中为每类操作提供一个函数,例如
- 在主进程中:
- 把 IPC handler 按领域组织,分别挂在对应服务模块上;
- 统一处理异常日志与返回格式。
重构完成后,IPC 层更像是:
- 一组明确的「本地服务接口」,而不是一堆自由发挥的消息字符串。
4. 反模式四:状态到处是,全局变量横飞
4.1 症状描述
常见表现包括:
- 主进程里用全局变量记录窗口、当前用户、配置等;
- 渲染进程中既有本地 state,又频繁通过 IPC 读写共享状态;
- 不同窗口各自维护一份类似状态,却没有清晰的同步机制。
结果是:
- 状态流向不清晰,各种「明明已经更新了,另一个地方却还是旧值」的问题频发;
- 调试时需要在主/渲染多个进程之间来回打印。
4.2 重构方向
在 Electron 应用中,状态可以按「范围」做粗粒度划分:
- 全局状态(与用户/会话关联):
- 放在主进程或后端服务中管理;
- 渲染进程通过 API 获取和修改。
- 窗口级状态:
- 放在各自渲染进程内部(前端状态管理库负责);
- 通过 IPC 通知主进程重要变化(例如某些需要跨窗口同步的信息)。
- 持久化状态(配置、缓存):
- 由本地服务模块负责读写;
- 提供「获取一次 + 订阅变更」之类的接口。
重构过程中,可以:
- 先列出所有全局变量和跨进程共享状态;
- 决定它们应该归属哪一层(主进程、本地服务、后端服务、窗口内部);
- 为每一类状态设计清晰的读写 API 和事件流。
5. 反模式五:把 Electron 当成「唯一后端」
5.1 症状描述
有些项目在一开始就把所有后端能力都塞进 Electron:
- 所有业务逻辑、数据存储、第三方集成都写在主进程/渲染进程里;
- 没有独立的服务端或云端 API,应用逻辑无法在桌面之外重用。
这在单机工具类应用里短期可行,但在以下场景中会遇到瓶颈:
- 需要多用户协作;
- 需要对接更多第三方服务;
- 需要同时支持 Web、移动端等其他客户端。
5.2 重构方向
在更大范围的系统里,更合理的分工通常是:
- 独立的后端服务:
- 负责多用户、持久化、鉴权、审计、复杂业务规则;
- 提供 HTTP/WebSocket 等接口。
- Electron:
- 作为桌面壳,提供本地增强(文件系统、本地缓存、原生集成);
- 同时调用「远端后端 + 本地服务」完成任务。
这种分层可以让:
- 桌面能力和通用业务逻辑解耦;
- 即便未来需要扩展 Web 客户端,也不必重写主要业务逻辑。
6. 小结:从反模式出发,收拢成几条架构原则
可以用几条总结这一篇的核心要点:
- 主进程应从「大杂烩」重构为「窗口管理 + 本地服务容器」;
- 渲染进程尽量像普通前端应用,通过受控 API 访问本地能力,而不是直接开启 Node;
- IPC 应当被视作一种本地 RPC 协议,集中定义通道和数据结构,并封装成清晰的客户端接口;
- 状态管理要按范围划分归属,避免全局变量横飞和跨进程随意读写;
- Electron 更适合作为桌面壳和本地增强层,而不是承载整个后端世界。
沿着这些方向逐步重构,Electron 项目可以从「杂乱但能跑」走向「可维护、可扩展、易于协作」,也更容易和现有的前后端架构对接起来。