当 Electron 应用从一个小工具成长为一个需要多人协作、长期维护的产品时,随意堆放的文件结构会成为开发效率的最大杀手。这一节不会给你一个“万能模板”,而是给出一个经过多个真实项目验证的目录设计思路,并解释每一个文件夹存在的理由。你可以根据自己的项目规模对其进行裁剪或扩展。
15.1.1 总体结构概览
一个典型的企业级 Electron 项目目录大致如下。它假设你使用 TypeScript 编写主进程和预加载脚本,使用 React/Vue 等框架编写渲染进程,并通过 electron-builder 完成打包。
my-app/
├── .github/ # CI/CD 工作流
├── build/ # 打包相关配置(图标、安装脚本)
├── resources/ # 额外资源文件(程序图标源文件等)
├── src/
│ ├── main/ # 主进程代码
│ ├── preload/ # 预加载脚本
│ ├── renderer/ # 渲染进程(前端项目)
│ ├── shared/ # 主进程与渲染进程共享代码
│ └── common/ # 通用工具(跨环境可用)
├── tests/ # 端到端测试、集成测试
├── docs/ # 内部文档
├── scripts/ # 开发辅助脚本
├── package.json
├── tsconfig.json # 主进程 TypeScript 配置
├── tsconfig.node.json # Node 环境配置(供 vite 等使用)
├── electron-builder.yml # 打包配置
└── .gitignore
这个结构并不是一开始就要全部创建好,而是在项目演进过程中逐步形成的。但提前了解全貌,能让你避免后期的重构痛苦。
15.1.2 src/main/ —— 主进程的家
主进程是应用的“后台大脑”,负责窗口管理、系统交互及进程通信中枢。建议将主进程代码按功能模块拆分,而不是把所有逻辑扔进一个 main.ts。
src/main/
├── index.ts # 入口:初始化 app,创建主窗口
├── window/ # 窗口管理
│ ├── mainWindow.ts # 主窗口创建与配置
│ ├── splashWindow.ts # 启动闪屏
│ └── windowManager.ts # 窗口生命周期统一管理
├── ipc/ # IPC 处理模块
│ ├── fileHandlers.ts # 文件读写、对话框等
│ ├── systemHandlers.ts # 系统信息、快捷键、托盘等
│ └── index.ts # 统一注册所有 IPC 处理器
├── services/ # 业务逻辑层
│ ├── updater.ts # 自动更新
│ ├── logger.ts # 日志记录(electron-log)
│ └── database.ts # 数据库连接(如 better-sqlite3)
├── utils/ # 主进程专用工具函数
└── constants.ts # 常量(如 IPC 通道名称)
为什么这样分:
- IPC 处理器独立成模块:当应用的功能增多,IPC 通道可能达到几十个。把它们按领域(文件、系统、用户)分别写在不同的文件里,并通过
index.ts统一注册,可以避免ipcMain.handle散落各处难以维护。 - 业务逻辑抽离到
services:让 IPC 处理器只负责参数校验和结果返回,真正的逻辑由 service 层完成。这样便于单元测试和逻辑复用。
15.1.3 src/preload/ —— 安全桥梁
预加载脚本是连接主进程和渲染进程的唯一安全通道。企业级项目通常会对预加载脚本进行分类,为不同的窗口暴露不同的 API。
src/preload/
├── main.preload.ts # 主窗口使用的预加载脚本
├── setting.preload.ts # 设置窗口(如果需要)
└── types.ts # 定义暴露给渲染进程的 API 类型
在 main.preload.ts 中,你会使用 contextBridge 暴露一个有限的对象,比如:
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => ipcRenderer.invoke('file:save', content),
// 绝不要暴露 ipcRenderer.send 或 on 的原始方法,
// 而是只提供封装好的语义化函数
});
关键原则: 渲染进程不知道 ipcRenderer 的存在,只知道 window.electronAPI 上那几个确切的方法。这既安全,又让前端开发者调用时有完整的类型提示(配合 types.ts 中的声明)。
15.1.4 src/renderer/ —— 前端的世界
渲染进程本质上就是一个标准的现代前端项目。你可以使用 Vite 创建 Vue 或 React 应用,然后把它放在 src/renderer/ 下。Electron 在开发时加载 Vite 的开发服务器地址,生产时加载打包后的静态文件。
src/renderer/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 图片、字体等
│ ├── components/ # 通用 UI 组件
│ ├── views/ # 页面视图
│ ├── hooks/ # 自定义 hooks(React)或 composables(Vue)
│ ├── stores/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.tsx # 根组件
│ └── main.ts # 入口文件
├── index.html
├── vite.config.ts
└── package.json # 前端依赖单独管理(可选)
要不要给渲染进程单独一个 package.json?
对于大中型项目,将前端依赖与 Electron 依赖分开是推荐的做法。这样做的好处是:
- 渲染进程的构建工具(Vite/Webpack)配置更简洁。
- package.json 中不会混杂
electron和vue/react的依赖,语义清晰。 - monorepo 工具(如 pnpm workspace)能轻松管理这种结构。
如果项目较小,把所有依赖放在根目录的 package.json 中也完全可以。
15.1.5 src/shared/ —— 代码共享层
主进程和渲染进程经常需要共享一些常量、类型定义甚至工具函数。将它们放在 shared/ 目录下,避免循环引用或环境冲突。
src/shared/
├── ipcChannels.ts # IPC 通道名称常量(两边都会引用)
├── types/ # 共享的类型定义
│ ├── file.ts
│ └── user.ts
└── utils/ # 与运行环境无关的工具函数
└── format.ts
例如,ipcChannels.ts 可以统一定义所有通道名,避免拼写错误:
export const IPC_CHANNELS = {
FILE_OPEN: 'file:open',
FILE_SAVE: 'file:save',
APP_GET_VERSION: 'app:getVersion',
} as const;
主进程注册 handler 时使用 IPC_CHANNELS.FILE_OPEN,渲染进程调用时也使用同一个对象,修改通道名时只需改动一处。
15.1.6 构建与配置层
build/目录存放打包相关的资源,比如 Windows 和 macOS 的应用图标(.ico, .icns)、安装程序的脚本(NSIS 配置)、代码签名证书路径等。electron-builder.yml是企业级项目普遍使用的打包配置文件。它远比在package.json中写"build"字段更清晰,支持多环境、多平台的条件配置。resources/用来放置构建时需要的额外文件,例如程序的图标源文件(.png, .svg),它们会被electron-builder根据配置转换成对应平台的格式。
15.1.7 测试目录 tests/
企业级项目不能没有自动化测试。建议至少在 tests/ 下包含:
- e2e/ — 端到端测试,使用 Playwright 或 Spectron 启动完整的 Electron 应用,模拟用户操作。
- integration/ — 主进程服务层的集成测试,直接调用 Node.js 模块。
- unit/ — 共享工具函数、预加载脚本逻辑的单元测试。
测试代码与源码分离,能保持项目目录清爽,也让 CI 流程更容易识别测试文件。
15.1.8 划分背后的真实考量
你可能会觉得这套结构有点“过度设计”,但真实情况是:一个中型以上的 Electron 应用,如果没有清晰的目录约定,不出三个月就会变成新人不敢碰的“遗产代码”。这些设计背后有几个朴素的工程原则:
- 关注点分离:主进程、预加载、渲染进程各司其职,不允许跨层级直接调用。shared 层打破这一隔离的唯一合法通道。
- 面向变化:把可能频繁修改的部分(UI、业务逻辑)和相对稳定的部分(IPC 通道定义、打包配置)分开,减少改动时的风险面。
- 易于协作:当你告诉新同事“文件相关的 IPC 处理全在
src/main/ipc/fileHandlers.ts”时,他就能立刻定位问题,而不是在一个上千行的main.js里翻找。 - 利于测试:将业务逻辑从主进程入口中剥离到 services 层,意味着你可以用 Jest 或 Vitest 直接测试它们,而无需启动整个 Electron 应用。
最终,目录架构不是教科书,而是一张根据项目实际需要不断打磨的地图。初学者可以从一个简单的单文件 main.js 开始,随着功能的增多,逐步向上面这种结构演进。关键是要在复杂度增长到失控之前,建立起这套秩序。