何时使用 Monorepo
Source: Dev.to

你真的需要 monorepo 吗?
对许多团队来说,诚实的答案是:一开始可能不需要——但这在很大程度上取决于你为何认为自己想要它。
大多数团队之所以会讨论 monorepo,是因为他们厌倦了在服务和前端之间复制代码和类型。情形很熟悉:你先有一个后端,再加一个前端,随后可能还有移动端应用,结果你在各处复制粘贴类型、请求/响应结构以及工具函数。你尝试抽取成一个 npm 包,却发现维护和版本管理非常痛苦,于是开始认真考虑像 Turborepo 或 Nx 这样的 monorepo 方案。
monorepo 是指一个仓库中包含多个应用、服务和库。Google、Meta 等公司让它们声名鹊起,而 Turborepo、Nx 等工具则把这种模式带给了日常团队。
典型的动机包括:
- 在多个项目之间共享代码和类型定义
- 简化依赖管理
- 统一工具链和 CI 流水线
- 更容易在整个代码库中进行重构
这些都是实际的好处——但它们也伴随着显著的权衡。
单体仓库的隐藏成本
尽管单体仓库很有吸引力,但它们并非免费。像 Nx 和 Turborepo 这样的工具功能强大,却会引入显著的复杂性和学习曲线。尤其是 Nx,充当一个复杂的构建系统和项目图管理器,提供任务图分析、增量构建与缓存以及插件生态系统。
要真正从 Nx 中受益,团队通常需要扎实掌握以下内容:
- TypeScript 项目引用
- 路径映射和模块解析
- 构建目标(
build、test、lint)的组合方式
对于经验不足的团队——或缺乏深厚 TypeScript 与工具链专业知识的团队——这可能成为真正的障碍。该工具在一切顺利时几乎像魔法一样,但一旦出现问题,调试就会变得困难。Nx 文档中展示的功能广度也暗示了这种复杂性。
随着时间推移,这可能会形成一个高度耦合的系统,简单的改动会产生广泛的连锁反应,增加风险和认知负担。工具链和 CI 也会变得具有挑战性:虽然单体仓库工具承诺更快的构建,但配置错误可能导致:
- 由于不必要的重建导致 CI 时间延长
- 难以调优的复杂缓存策略
- 难以追踪的依赖导致意外的破坏
如果没有精心调校的 CI 和开发工具,你最终可能比使用多个更简单的仓库还要糟糕,削弱了采用单体仓库的初衷。
单体仓库的替代方案
在决定采用单体仓库之前,许多团队会尝试使用共享 npm 包来避免代码重复。他们将公共代码抽取到类似 @your-org/api-types 或 @your-org/utils 的包中,托管在私有注册表(例如 GitHub Packages 或自建 npm),并在前端和后端项目中消费这些包。
这种做法解决了一些问题,但也带来了新的挑战:
- 版本管理开销——每一次更改都需要提升版本、发布并更新所有使用该包的应用。
- 漂移——如果后端和前端不同步升级,可能会出现细微的不兼容。
- 基础设施成本——维护私有注册表、认证以及共享库的独立 CI 流水线。
团队选择单体仓库的核心原因之一是希望避免在各处重复编写相同的类型和客户端代码。与其手动维护共享的 TypeScript 库,另一种方案是从 API 规范生成类型和客户端代码。
如果你的 API 已经通过 OpenAPI 或 Swagger 进行良好描述,你可以为每个使用方生成类型和客户端代码。像 Hey API 这样的工具可以读取 OpenAPI 规范并输出 TypeScript 类型以及面向 Web、移动端等环境的 API 客户端代码。在这种模式下,OpenAPI 文档成为后端与前端之间的契约,用共享规范取代共享的 TypeScript 代码。
基于 API 规范的开发的好处
- API 契约的唯一真实来源
- 自动生成最新的类型和客户端代码
- 减少服务之间手动同步的需求
- 通过代码生成器(如 OpenAPI Generator、
openapi-typescript)兼容多语言和多平台
Source: make-it.run
当使用 Monorepo 合理时
在某些明确的情境下,Monorepo 可能值得增加的复杂度。如果你有多个正在积极开发且大量共享代码的应用程序,应该认真考虑使用 Nx、Turborepo 或类似工具的 Monorepo,例如:
- 一个管理后台、一个面向公众的网页应用、若干后端服务以及一个共享的 UI 库
- 频繁的跨项目协作和重构
- 需要在整个代码库中保持一致的 lint、测试和构建流水线
在这种环境下,代码复用程度很高,统一仓库的好处可以抵消其带来的额外开销。