Electron 架构怎么定:按项目规模选模块化方案的完整做法
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
Electron 应用做到两三万行之后,最常见的状况是:窗口逻辑、业务状态、IPC 消息全挤在同一个入口文件里,每次加功能都要通读一遍主进程代码才敢动手。这篇内容直接给出三样东西:不同规模项目该选哪种架构的判断表、进程边界与 Context Bridge 这套核心机制的拆解,以及可以直接照抄的目录划分、依赖治理和性能手段。
先定架构:按项目规模选,而不是先学模式
📐 先回答"选哪种",再展开"怎么实现"。判断依据是代码量、窗口数量和团队协作方式,对照下面这张表即可定位:
| 项目特征 | 推荐架构 | 目录形态 | 进程通信方式 |
|---|---|---|---|
| 单窗口、个人项目、5k 行以内 | 单体内分区 | main、renderer 各一个目录 | 直接 ipcRenderer.invoke,无需封装 |
| 多窗口、2~5 人、3w 行以内 | 水平分层 + 按功能拆模块 | main / renderer / common 三层,功能在层内纵向切 | 每个功能模块自定义通道前缀 |
| 十万行级、多团队 | 壳应用 + 微应用 | shell 管窗口与导航,各业务独立构建 | 模块只与壳通信,禁止模块互调 |
规则只有一条:规模不够别上重型结构。给个人工具引入微前端,维护成本比代码本身还贵;反过来,十万行的应用还靠一个入口文件撑,任何人都不敢动第二行。
机制深挖:进程边界是 Electron 架构的地基
Electron 的架构问题几乎都出在进程边界的模糊上。主进程单实例存在,独占 app 生命周期、窗口管理、菜单、系统通知这类原生能力;渲染进程则按页面数量多开,只负责 UI 与页面逻辑。判断一段代码该放哪边,就看它是否需要操作系统级资源——需要归主进程,不需要归渲染进程。
Electron 本身就是这么约束的:主进程能用的模块和预加载脚本能用的模块是两份完全不同的清单。主进程模块清单里登记了 app、BrowserWindow、ipcMain、Menu、utilityProcess 等几十项,全部是单例级的系统能力;而 预加载模块清单只有 contextBridge、ipcRenderer、nativeImage 三项。这份刻意收窄的白名单,等于替每个项目提前划好了进程边界的底线:渲染侧永远拿不到 Node 的系统 API,一切能力必须过桥。
Context Bridge:渲染侧唯一合法的取数通道
结论先行:页面代码不应该认识 ipcRenderer,只应该认识 preload 里暴露出来的那组函数。
Electron 仓库里的实现(lib/renderer/api/context-bridge.ts)在暴露 API 前会强制校验 contextIsolation 是否开启,隔离没开就不让调用。使用时保持"窄暴露"原则:
// preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('appApi', { getUser: (id) => ipcRenderer.invoke('user:get', id) })// 主进程 const { ipcMain } = require('electron') ipcMain.handle('user:get', (_event, id) => userService.fetch(id)) // 页面侧 const user = await window.appApi.getUser(1)为什么必须这么写:invoke/handle 是请求响应配对,天然带返回值和错误处理,适合所有读操作;暴露的函数签名即页面 API,后续改实现不影响 UI 层。反模式是把整个 ipcRenderer 塞进 window,或者在 preload 里写通用代理转发任意通道——后者等于把白名单机制整个拆掉。
三种架构形态,跟着规模递进讲
小型:一个目录解决的事不要拆两个。主进程入口、preload、页面代码各归其位即可,IPC 通道名用功能:动作的命名习惯(如window:minimize)防止日后乱撞。此阶段的架构决策只有两个:开不开 contextIsolation(开),要不要封装 IPC(不要)。
中型:分层与功能切分并行。水平方向分三层——main 放窗口和生命周期,renderer 放 UI 与状态,common 放两边都要用的常量、类型、工具函数(Electron 源码里的 common 模块 就是跨进程共享代码的现成范例)。垂直方向按功能切模块:每个业务模块内部再分自己的主进程代码、渲染端代码和共享类型,并只通过模块的 index 出口对外,模块间不允许直接 import 内部文件。这样"改登录不影响编辑器"就有了物理保障。
超大型:壳应用 + 微应用。窗口创建、路由、系统托盘收进 shell;仪表盘、编辑器、设置等拆成独立构建、独立部署的子应用,各自维护自己的 preload 与更新节奏。关键纪律是:微应用之间不直接通信,所有跨模块调用走壳的 IPC 通道中转。这牺牲了一点调用效率,换来的是任何业务模块崩溃或下线都不波及全局——这个代价只有多团队协作时才划算。
工程落地:目录、依赖、性能一次说清
Electron 主进程目录怎么划分。以进程为第一级目录(main / renderer / common),功能为第二级,入口文件 main.ts 只做三件事:初始化、注册 IPC 总表、创建窗口。业务逻辑下沉到 services 与 ipc 两个子目录,前者管数据与副作用,后者只做参数校验与转发。common 目录只允许纯函数与类型,禁止出现任何require('electron')。
依赖关系怎么管。出现循环依赖的信号是构建报错或模块加载时拿到 undefined,解法是引入事件总线或中介者:A 不再 import B,而是监听 B 发出的事件。重计算任务(转码、索引、大批量解析)挪到 utilityProcess 跑,utilityProcess 源码 里可以看到它本质上就是开了一个独立的 Node 子进程,主进程不陪跑、渲染进程不卡顿。
性能手段怎么选。三个手段按优先级排:大模块动态 import,菜单、设置面板这类低频功能延迟到触发时才加载;关键资源随应用启动预载;复杂页面拆到独立视图进程隔离渲染,避免一个标签页卡死整个窗口。顺序别反——先确认模块划分正确,再谈加载时机优化,否则动态 import 只是给混乱的代码换了个出场方式。
避坑清单与源码入口
| 坑 | 后果 | 正确做法 |
|---|---|---|
| IPC 传函数或 DOM 对象 | 结构化克隆直接报错 | 只传可序列化的纯数据 |
| 为图方便关掉 contextIsolation | 页面可直达 Node API,安全模型崩塌 | 保持开启,能力一律走桥 |
| preload 顶层写异步初始化 | 时序竞争,API 偶发 undefined | 异步逻辑放进暴露的方法内部 |
| 模块间横向 import 内部文件 | 改 A 连带崩 B | 只暴露 index 出口 |
| 主进程同步阻塞(大量文件 IO 等) | 所有窗口 UI 冻结 | 阻塞操作挪到 utilityProcess 或 child_process |
想继续往源码里挖,三个入口够用:进程模型的完整说明在 docs/tutorial/process-model.md,安全侧的上下文隔离原理在 docs/tutorial/context-isolation.md,主进程到底有哪些原生模块则直接翻 lib/browser/api/module-list.ts。
文中涉及的模块清单与接口行为均以 Electron 官方仓库源码为准,不同版本间预加载白名单和视图 API 可能有差异,落地前建议对照所用版本核对一遍。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考