项目目录结构设计:单体仓库 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 图把本章的核心逻辑串起来了——从仓库策略选择,到目录模板,再到命名规范,最后落到层级约束:

项目目录结构设计核心逻辑 仓库策略选择 Monorepo(单体仓库) Multirepo(多仓库) 标准目录模板 命名规范(kebab / Pascal / camel) 层级约定(不超过 4 层) 可维护、可扫描、可自动化的目录体系 选择策略 → 模板 → 规范 → 约束

你看,整个决策链条是自上而下的:先定仓库策略,再套目录模板,然后用命名规范和层级约定把细节锁死。每一步都影响下一步,缺一环就容易乱。

最后说一句:目录结构不是写一次就完事的。我每半年会带着团队做一次「目录健康检查」——看看有没有超过 4 层的嵌套、有没有命名不一致的地方、有没有废弃的文件夹。这就像定期打扫房间,不扫就会积灰。