2026年的 Monorepos 与 Polyrepos:小型团队的实际选择
每隔几年,monorepo 与 polyrepo 的辩论就会以双方新的坚定立场重新出现。到2026年,对话已经转变。Google、Meta 和 Microsoft 以行星规模运行 monorepo,而支持这种模式的工具已经普及到五人团队。与此同时,polyrepo 方则指出微服务架构,认为仓库边界强制执行了 monorepo 试图提供的模块化。
大多数小型团队实际面临的问题更简单也更紧迫:在未来十八个月内,哪种方法会带来更少的痛苦?答案取决于与 Google 所做的无关的因素。
定义术语,避免行话
monorepo 是一个单一的版本控制仓库,包含多个不同的项目、服务或包。这些项目可能相关,也可能不相关。其定义性特征是共享历史和共享工具,而非共享代码。
polyrepo(有时称为”多仓库”)是大多数团队开始时的默认选择:每个项目、服务或库都存在于自己的仓库中,有自己的 CI 管道、自己的版本控制和自己依赖声明。
人们常忽略的关键区别:monorepo 不是单体架构。单体架构是关于代码如何部署的架构选择。monorepo 是关于代码如何组织的源代码控制选择。你可以拥有一个包含可独立部署的微服务的 monorepo。你也可以拥有一个每个服务都紧密耦合的 polyrepo 设置。仓库结构并不决定你的架构,尽管它确实以重要方式影响你的工作流程。
2026年的工具生态
monorepo 工具生态系统已经显著成熟,这种成熟度既是卖点也是复杂性的来源。
Turborepo
2022年被 Vercel 收购后,Turborepo 已成为 JavaScript 和 TypeScript monorepo 的默认入口点。其远程缓存和任务管道系统使得定义包之间的构建依赖变得简单。对于已经在 Vercel 上或主要在 Node.js 生态系统中工作的团队,Turborepo 提供了进入 monorepo 领域的最小阻力路径。权衡之处在于:它深度以 JavaScript 为中心。如果你的堆栈包含 Go 服务、Python ML 管道或 Rust CLI 工具,以及你的 TypeScript 前端,Turborepo 开始感觉像方头钉进圆孔。
Nx
Nx 已经从专注于 Angular 的工具演变为通用的 monorepo 编排器。它的项目图分析、受影响命令检测和插件生态系统使其成为需要复杂任务调度的团队功能最丰富的选择。Nx Cloud 提供分布式缓存和 CI 分发。学习曲线比 Turborepo 陡峭,但对于拥有三十个或更多包的仓库来说,回报是实实在在的。对于一个管理六个包的三人团队来说,Nx 可能就像开着叉车去移动书架一样大材小用。
Bazel
Bazel 仍然是跨多语言代码库中用于密封、可重现构建的黄金标准。通过其规则系统,它可以处理 Java、Go、Python、C++ 以及几乎所有其他语言。代价是显著的:Bazel 的 BUILD 文件语法、沙盒模型和陡峭的学习曲线使其没有专门的构建基础设施专业知识的小团队难以适应。如果您是一个五人创业公司正在评估 Bazel,那么您几乎肯定是在过度设计您的构建系统。
pnpm Workspaces
对于那些想要获得 monorepo 好处但又不想使用 monorepo 工具的团队,pnpm workspaces 提供了一个务实的折中方案。结合 pnpm 的内容可寻址存储和严格的依赖解析,workspaces 让您可以在包之间共享代码,而无需过多繁琐的配置。没有任务运行器,没有构建图,没有远程缓存。只有一个仓库中的链接包。这通常已经足够,而从这个方案开始的团队往往只在遇到特定的痛点而非预期的问题时才会添加 Turborepo 或 Nx。
Monorepo vs Polyrepo:对比
| 因素 | 单体仓库 | 多仓库 |
|---|---|---|
| 代码共享 | 简单直接。直接从兄弟包导入。 | 需要将包发布到注册表或使用 git 子模块。 |
| 依赖管理 | 单个锁文件。统一的依赖版本。更容易审计。 | 每个仓库管理自己的依赖。版本漂移很常见。 |
| CI/CD 复杂度 | 需要智能过滤以避免每次提交都构建所有内容。 | 每个仓库都有简单、自包含的流水线。 |
| 入职体验 | 克隆一次,看到所有内容。可能会让人不知所措。 | 只克隆你需要的内容。上下文范围更窄。 |
| 原子性变更 | 单个 PR 可以同时更新 API、客户端库和使用者。 | 需要跨仓库协调 PR,并仔细合并顺序。 |
| 访问控制 | 默认情况下每个人都能看到所有内容。GitHub CODEOWNERS 有帮助但不够精细。 | 自然隔离。仓库级别的权限很直接。 |
| 工具开销 | 需要支持单体仓库的工具(Nx、Turborepo 等)。 | 标准的 git 工作流。不需要特殊工具。 |
| 重构 | 跨项目重构在单个提交中完成。 | 跨仓库重构需要迁移计划和版本化发布。 |
| Git 性能 | 随历史记录大小而下降。稀疏检出有帮助但增加了摩擦。 | 每个仓库保持精简。Git 操作保持快速。 |
| 发布管理 | 需要 Changesets 或 Lerna 等工具进行独立版本控制。 | 每个仓库有自己的发布周期。简单且独立。 |
CI/CD:真正的成本所在
对于小团队来说,单体仓库和多仓库之间最大的实际差异不是代码共享或依赖管理,而是 CI/CD。
在多仓库设置中,每个仓库都有专门的流水线。推送到 API 仓库,只运行 API 测试。推送到前端仓库,只构建前端。流水线简单、编写快速且易于理解。初级开发者可以在十五分钟内理解 CI 配置。
在单体仓库中,每次推送都可能影响所有内容。如果没有受影响文件检测,你的 CI 会在每次提交时运行所有测试套件和所有构建。Turborepo 和 Nx 通过它们任务图和缓存系统解决了这个问题,但这些系统并非没有成本。你需要正确配置它们,随着项目的发展维护配置,并在缓存产生过期结果时进行调试。远程缓存又增加了一个变动因素。分布式任务执行则增加了另一个。
对于一个每天推送十次提交的两到三名工程师团队来说,多仓库的 CI 简化是一个真正的优势。配置和维护单体仓库 CI 工具所花费的时间可能会超过原子级跨项目更改所节省的时间。
依赖管理与版本漂移问题
单体仓库最有力的论点是统一的依赖管理。当你的 React 前端、共享组件库和 API 客户端都存在于同一个仓库中时,它们共享一个锁文件。升级 React 意味着在一个 PR 中 everywhere 升级,运行一次测试覆盖所有使用者。
在多仓库世界中,依赖漂移是默认状态。你的前端使用 React 19.1,你的组件库仍在 18.3,而你的内部工具仓库已有八个月未更新,运行的是 React 17。每次升级都是一项独立的工作,需要自己的 PR、自己的测试运行和自己的部署。对于快速迭代的小团队来说,这种漂移会悄无声息地累积,直到生产环境中出现故障,因为两个服务对共享类型定义存在分歧。
然而,统一依赖是一把双刃剑。在单体仓库中,共享工具库中的破坏性更改会同时影响所有使用者。如果你的共享验证库引入了一个错误,导入它的每个服务都会在同一个提交中损坏。多仓库的版本固定提供了自然的断路器:修复验证后,每个服务可以按照自己的时间表升级共享库。
真正有效的代码共享模式
小团队通常通过三种模式共享代码,单体仓库与多仓库的选择对每种模式的影响不同:
共享工具库
日期格式化、验证助手和 API 客户端包装器等功能。在单体仓库中,这些作为内部包直接导入。在多仓库设置中,它们需要发布到 npm(或私有注册表)并进行版本管理。单体仓库方法迭代更快。多仓库方法迫使你考虑库的 API 表面,这通常会产生设计更好的代码。
共享类型和契约
TypeScript 接口、API 模式、protobuf 定义。这正是单体仓库真正大放异彩的地方。通过一次提交更改 API 响应格式并更新所有消费者,可以消除整个类别的集成错误。在多仓库环境中,保持类型同步需要从共享模式仓库生成代码或进行谨慎的手动协调。
共享配置
ESLint 配置、TypeScript 编译器设置、Docker 基础镜像。即使在单体仓库中,这些通常也最好作为已发布的包进行管理。作为内部单体仓库包存在的共享 ESLint 配置可以正常工作,但作为被独立仓库使用的 npm 包也能完美运行。单体仓库在这里的优势微乎其微。
“小规模单体仓库”陷阱
这是小型团队中反复出现的模式:三到五名开发者阅读了关于单体仓库的好处,设置了 Turborepo 或 Nx,将他们现有的两三个仓库迁移到单个仓库中,然后在接下来的一个月里与工作区配置、CI 管道更改和 IDE 性能问题作斗争。六个月后,单体仓库包含与开始时相同的项目。没有创建新的共享包。证明迁移合理的原子跨项目提交每个季度只发生一次。
这个陷阱是为优化一个你实际上并不使用的工作流程。当你频繁进行跨多个包的更改时,单体仓库提供最大的价值。如果你的前端团队和后端团队很少接触对方的代码,那么单体仓库只会增加开销,而无法提供其核心价值。
小型团队尤其容易受到影响,因为迁移成本低到看似微不足道。将三个仓库合并成一个只需要一个周末。但是,维护单体仓库工具、教育新团队成员关于工作区约定以及调试构建图问题的持续成本是持续支付的。对于一个每个人已经知道要推送到哪个仓库以及 CI 如何工作的团队来说,这种持续成本需要持续的好处来证明其合理性。
最好的仓库结构是你的团队无需专门的工具专业知识就能维护的结构。对于大多数小型团队来说,这就是他们已有的结构。
小型团队何时真正适合使用单体仓库
尽管有上述警告,但在某些情况下,单体仓库显然是小型团队更好的选择:
- 全栈 TypeScript 应用。 如果你的前端、后端和共享库都使用 TypeScript,那么工具链的摩擦会很小。Turborepo 或 pnpm workspaces 可以处理工作区管理,而客户端和服务器之间的共享类型可以消除一类真正的 bug。
- 需要一起发布的多个包。 如果你的 API 客户端、SDK 和 CLI 工具始终以相同版本发布并共享大量实现代码,那么 monorepo 可以自然地保持它们同步。
- 快速原型阶段。 当项目边界尚不明确,代码经常在包之间移动时,monorepo 可以减少这种探索的摩擦。当架构稳定后,你随时可以拆分为独立的仓库。
- 共享设计系统。 由多个应用程序使用的组件库从 monorepo 式的开发中获益匪浅,因为组件变更可以在合并前针对每个使用它的应用程序进行测试。
何时选择多仓库架构
- 多语言技术栈。 如果你的团队运行 Go API、Python 数据管道和 React 前端,那么 monorepo 的工具链会变得复杂。每种语言都有自己的构建系统、依赖管理器和测试框架。虽然可以在一个 monorepo 工具下统一它们,但在小规模项目中很少值得这样做。
- 拥有明确所有权边界的团队。 如果一名开发者拥有移动应用,另一名开发者拥有后端,他们通过稳定的 API 合约进行通信,那么独立的仓库能反映这种现实,减少代码审查中的噪音。
- 开源组件。 如果你的任何包需要开源,它们无论如何都需要自己的仓库。从 monorepo 中提取一个包到独立仓库比从一开始就保持分离更费力。
- 合规性和访问控制。 如果你的代码库不同部分有不同的访问需求,独立的仓库提供了清晰的权限边界,而像 CODEOWNERS 这样的 monorepo 解决方案无法完全复制这种边界。
2026 年的决策框架
不要默认采用任何一种方法,而是考虑以下问题:
- 你多久进行一次跨项目变更? 如果答案是每周或更频繁,那么单体仓库的代价是值得的。如果答案是每月或更少,那可能就不值得了。
- 你的技术栈是否统一? 全是 TypeScript、全是 Go、全是 Python?单体仓库工具运行良好。混合技术栈?工具成本会急剧增加。
- 团队中有人喜欢构建工具吗? 单体仓库需要持续维护工作区配置、CI 管道和构建图。如果团队中没有人对这项工作感到满意,它将被忽视成为基础设施。
- 你的部署目标是否独立? 如果服务在不同时间独立部署,多仓库的自然隔离与你的部署模型一致。如果一切一起部署,单体仓库简化了这种协调。
- 你的团队增长轨迹如何? 一个在明年从三人增长到十五人的团队可能受益于尽早建立单体仓库约定。一个将保持在五人的团队可以优化简单性。
务实的中间地带
2026年,许多成功的小团队正在运行一种混合方法,这种方法很少在单体仓库与多仓库的讨论中被提及。模式是这样的:一个主要单体仓库用于密切相关的应用程序代码(前端、BFF、共享库),而基础设施配置、独立工具以及任何具有不同访问或发布要求的代码则放在单独的仓库中。
这种混合方法避免了两种方法的最坏故障模式。你在最重要的地方获得原子化的跨项目提交,而不必将无关代码强制放入共享构建系统。主要单体仓库保持足够的专注,使工具保持可管理性,而卫星仓库保持足够的简单,不需要单体仓库式的编排。
pnpm workspaces 处理单体仓库部分。标准的 git 工作流处理独立仓库。不需要维护 Nx 配置,不需要编写 Bazel BUILD 文件,也不需要操作远程缓存基础设施。当团队增长或代码库复杂性增加时,升级到 Turborepo 或 Nx 是一个简单的迁移,因为工作区结构已经就位。
对于小团队来说,单体仓库与多仓库的决定归结为成本效益分析,这会根据你的具体情境而变化。做对这件事的团队不是那些选择了理论上最优结构的团队。而是那些选择了一种能够与当前团队、当前工具专业知识和当前跨项目变更率高效运作的结构的团队。在2026年,工具已经足够好,可以使两种方法都可行。问题是哪一种方法对你实际拥有的团队来说摩擦最小。
