项目目录结构设计:单体仓库 vs 多仓库
说到多项目并行的文件管理,第一个绕不开的问题就是:代码到底放一个仓库里,还是拆开放? 这就像问一个团队是住集体宿舍还是各自租房——没有绝对的对错,但选错了会非常难受。
我见过不少团队,一开始图省事全塞一个仓库,结果 CI 跑一次两小时;也见过拆得太碎,光找依赖就得翻十几个仓库。今天咱们就把这事掰扯清楚。
单体仓库(Monorepo)与多仓库(Multirepo)的抉择
先看一张对比表,把核心差异说透:
| 维度 | 单体仓库(Monorepo) | 多仓库(Multirepo) |
|---|---|---|
| 代码可见性 | 所有项目代码在同一个仓库内 | 每个项目独立仓库,按需授权 |
| 依赖管理 | 统一版本,一次构建 | 各自管理,版本同步成本高 |
| CI/CD 效率 | 全量构建慢,但可增量优化 | 独立构建快,但跨项目联调麻烦 |
| 重构成本 | 跨项目重构极方便 | 跨仓库改动需要多 PR 协作 |
| 权限控制 | 颗粒度较粗 | 仓库级精细控制 |
| 新人上手 | 一次 clone,全量代码 | 需要逐个 clone,配置环境 |
我的建议:如果团队在 20 人以内,项目间共享公共库(比如 UI 组件、工具函数),优先选 Monorepo。超过 50 人、或者有严格的第三方安全审计要求,再考虑 Multirepo。
为什么?我在上一家公司带过 8 个前端微服务并行开发。一开始用了 Multirepo,结果公共组件改了接口,6 个仓库的负责人挨个发 PR、等 review、等发布……一个版本升级拖了两周。后来切到 Monorepo,用 Lerna + Yarn Workspaces,一次提交搞定所有依赖升级。说白了,沟通成本往往比技术成本更贵。
标准目录模板设计
不管你选哪种仓库策略,目录结构必须有一致性。我习惯用下面这套模板,它经过了 3 个团队、4 年的迭代验证:
project-root/
├── packages/ # 子项目/模块(Monorepo 核心)
│ ├── project-a/
│ ├── project-b/
│ └── shared-lib/
├── docs/ # 项目文档
│ ├── architecture/
│ ├── api/
│ └── decisions/ # 架构决策记录(ADR)
├── scripts/ # 构建/部署脚本
│ ├── build.sh
│ ├── deploy.sh
│ └── lint.sh
├── config/ # 全局配置
│ ├── eslint.config.js
│ ├── tsconfig.base.json
│ └── jest.config.js
├── .github/ # CI/CD 工作流
│ └── workflows/
├── .gitignore
├── package.json # 根 package.json(Workspaces)
├── lerna.json # Monorepo 编排(如使用)
└── README.md
一个小技巧:根目录下不要放任何业务代码。它只负责「编排」——告诉工具链去哪里找项目、用什么配置、怎么构建。这样新人一看根目录就知道「哦,这是总控台」。
每个子项目内部,我推荐统一成这样的结构:
packages/project-a/
├── src/ # 源代码
│ ├── components/
│ ├── services/
│ ├── utils/
│ └── index.ts
├── tests/ # 测试文件(与 src 结构镜像)
│ ├── components/
│ └── services/
├── public/ # 静态资源
├── dist/ # 构建产物(不提交 git)
├── node_modules/ # 依赖(不提交 git)
├── package.json
├── tsconfig.json
└── README.md
嗯,这里要注意:tests 目录不要混在 src 里面。我以前也试过把测试文件放在组件旁边,结果构建时总得加一堆 exclude 规则,烦得很。分开之后,测试框架直接指向 tests/,干净利落。
命名规范与层级约定
命名这事,看着小,但坑最多。我曾经因为一个文件名大小写问题,在 Linux CI 上排查了整整半天——本地 Mac 不区分大小写,服务器上直接 404。
下面是我总结的「铁律」:
- 目录名:全小写 + 连字符(kebab-case)。例如
user-profile/、api-gateway/。不要用下划线,不要用驼峰。 - 组件/类文件:大驼峰(PascalCase)。例如
UserCard.tsx、ApiClient.ts。 - 工具/函数文件:小驼峰(camelCase)。例如
formatDate.ts、parseConfig.ts。 - 样式文件:与对应组件同名。例如
UserCard.module.css。 - 测试文件:源文件名 +
.test或.spec。例如formatDate.test.ts。
避坑指南:我曾经在一个 Monorepo 里看到有人把目录命名为 UserProfile,另一个项目里又写成 user_profile。结果 CI 脚本里写的是 user-profile,三个名字三个写法,构建直接炸裂。所以,命名规范必须写进 README,并且用 lint 工具强制检查。
层级约定:不要超过 4 层
目录嵌套太深是另一个常见病。我见过 src/components/common/buttons/primary/PrimaryButton.tsx——光找这个文件就得点 5 次文件夹。你想想看,这得多浪费时间?
我的经验是:任何目录的嵌套层级不要超过 4 层。超过 4 层,要么是抽象错了,要么是命名太啰嗦。
举个例子:
- ✅
src/components/UserCard.tsx(2 层) - ✅
src/components/user/UserCard.tsx(3 层) - ❌
src/components/common/buttons/primary/PrimaryButton.tsx(5 层)
如果确实需要细分,我建议用「扁平化 + 前缀」的方式:
src/
├── components/
│ ├── UserCard.tsx
│ ├── UserCardHeader.tsx
│ └── UserCardFooter.tsx
而不是:
src/
├── components/
│ └── user/
│ └── card/
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── index.tsx
说白了,扁平结构比嵌套结构更容易被人类大脑扫描。你每次找文件少点两下鼠标,一年下来省下的时间够看一部电影了。
知识体系总览
下面这张 SVG 图把本章的核心逻辑串起来了——从仓库策略选择,到目录模板,再到命名规范,最后落到层级约束:
你看,整个决策链条是自上而下的:先定仓库策略,再套目录模板,然后用命名规范和层级约定把细节锁死。每一步都影响下一步,缺一环就容易乱。
最后说一句:目录结构不是写一次就完事的。我每半年会带着团队做一次「目录健康检查」——看看有没有超过 4 层的嵌套、有没有命名不一致的地方、有没有废弃的文件夹。这就像定期打扫房间,不扫就会积灰。