monorepo 对话经常从错误的地方开始。有人问是拥有一个存储库更好还是拥有多个存储库更好,就好像这是关于组织文件夹的决定一样。它不是。这是关于团队如何协调工作的决定,而存储库只是其中可见的部分。
通过 [TypeScript0,这个决定获得了一种特定而强大的风格:在同一空间内的前端、后端和内部库之间共享类型的可能性。这是让认真的团队考虑单一存储库的论点,也是隐藏成本的论点,你稍后才会发现。
本文适用于那些决定架构和团队组织并需要在签约之前权衡双方的人。
真正的吸引力:实时合约类型
使用 TypeScript 的单一存储库最具体的好处是系统两端之间共享的类型。后端定义响应的格式,前端使用相同的格式,并且它们都指向相同的定义。
当后端改变数据格式时,前端立即停止编译。没有协调会议,没有过时的合同文档,没有 API 更改的经典错误,也没有人警告接口。编译器成为团队之间的协调机制。
这解决了分布式系统中最大的摩擦来源之一:数据生成者和数据使用者之间的脱节。在单独的存储库中,合同存在于信任和文档中。在类型化 monorepo 中,合约存在于代码中,并在每次提交时进行检查。任何想要进一步实现从银行到接口的保证的人都值得关注 tRPC、Drizzle 和 Prisma2 的端到端类型安全性。
monorepo 还提供什么
除了类型之外,还有值得一提的协调增益。跨前端和后端的更改适合单个提交和单个审查,而不是成为存储库之间同步的拉取请求的舞蹈。
标准化也更容易。一种 lint 配置、一种版本的 TypeScript、一组适用于所有内容的格式化规则。团队没有将每个存储库分支为自己的方言,而是保持了独特的技术文化,这降低了项目之间移动的成本。
还有内部代码的重用。一个组件库、一组实用函数、服务于多个应用程序的域规则。在 monorepo 中,这是每个人在当前版本中使用的内部包,无需在每次更改时发布和更新依赖项。
一开始就没有人表现出的权衡
现在故事的另一半是,为什么做得好的 monorepo 很强大,而做得不好的 monorepo 却是一个锚点。
第一个成本是构建成本。当一切都在一起时,您需要能够了解已更改内容并仅重建必要内容的工具,否则每个小更改都会触发一个缓慢的过程,该过程会随着存储库的增长而增长。如果没有智能缓存和增量构建,管道时间就会成为团队的日常抱怨。
第二个成本是治理。任何人都可以从任何地方导入任何内容的存储库很快就会退化为交叉依赖关系的混乱。前端最终导入了一些只在后端有意义的东西,而纸上存在的分离在实践中消失了。 Monorepo 需要明确的界限和维护界限的纪律。
第三个成本是更少的技术成本和更多的人力成本。单个存储库意味着单个协调点。权限、代码所有者、审核、发布流程。所有这些都开始生活在同一个空间中,大型团队需要明确的所有权规则,以避免一直互相踩踏。
当它真的值得的时候
当您查看问题的概况而不是跟随趋势时,决策就会变得更简单。当前端和后端属于同一团队或非常接近的团队并且经常一起更改时,带有 [TypeScript4TM 的 Monorepo 会表现出色。
当应用程序之间存在真正共享的域代码时,它就会发光,并且在单独的存储库中保持同步的成本已经很严重了。当层之间的契约变化足够大以至于自动类型检查足以支付构建框架的投资时,它就会发光。
另一方面,如果系统真正独立,以不同的速度发展,并且属于彼此几乎不交流的团队,那么强制使用单一存储库会在不存在耦合的情况下产生耦合。你付出了复杂性的代价,却没有获得协调,因为没有协调可以收集。在这种情况下,将类型发布为版本化包的单独存储库通常效果更好。
采用之前要考虑什么
在移动任何东西之前,值得诚实地回答几个问题,因为这些问题的答案决定了 monorepo 是否会帮助或阻碍您。
首先是关于工具成熟度。您是否拥有或愿意维护健康的 monorepo 所需的增量构建和缓存基础设施?如果没有这个,管道时间将削弱协调增益。这是一项持续的投资决策,而不是一次性的设置。
二是关于边境纪律。团队是否愿意定义和强制执行包之间的边界,即使当时跨越边界似乎更快?没有治理的 Monorepo 会变成更大规模的意大利面条代码。
第三个是关于你正在解决的问题。您采用 monorepo 是因为缺乏共享类型导致了真正的错误和摩擦,还是因为它已经成为标准并且看起来很有组织?第一个原因证明了成本的合理性。第二个几乎从来没有。
该决定是组织的决定,而不是文件夹的决定
最后,使用 [TypeScript5 的单一存储库是调整团队工作方式的选择,共享类型充当他们之间的技术粘合剂。当团队确实需要一起工作时,它是最有效的结构之一。当他们不需要它时,它的复杂性就会伪装成良好的实践。
做出良好决定的技术领导力是将真正的收益(编译器可验证的协调)与将所有内容集中在一处的美观性分开的技术领导力。第一个理由证明了投资的合理性。第二个是昂贵的陷阱。
如果您正在权衡这个决定,请首先弄清楚您的团队今天如何一起更改代码以及摩擦在哪里。存储库结构必须遵循工作的实际情况,而不是相反。为了全面了解类型如何支撑整个系统,值得从[为什么 TypeScript 成为标准 6] 开始。
另请阅读
- [高级 TypeScript 实践:浅度使用与成熟使用的区别7
- [为什么 TypeScript 成为现代网络的标准8
- [使用 Zod 进行数据验证:为什么 TypeScript 类型还不够9
- [服务器优先:减轻浏览器负担的架构决策10
- [端到端类型安全:从工作台到前端,不破坏边界11
- [微前端:何时以及为何在可扩展项目中采用12
