深入解析Monorepo:超越单一代码仓库的技术架构

本文深入探讨了Monorepo的技术本质,解析了原子提交、统一目录结构、单一版本规则等核心概念,比较了与多仓库系统的差异,并分析了在实际开发环境中的优缺点。

什么是真正的Monorepo?

在软件公司中,经常会有关于是否应该采用"Monorepo"的讨论,即"公司所有代码都存放在一个版本控制的仓库中"。通常,人们基于Google如何存储代码来做这个决定。

我现在已经在拥有非常先进的Monorepo系统(Google)和非常先进的多仓库系统(LinkedIn)的开发者生产力组织中工作过,我必须告诉你:人们与Monorepo关联的大多数有价值特性与你有多少个源代码控制仓库无关。

事实上,人们(和Google)认为的Monorepo实际上是多个不同的概念:

  • 跨项目的原子提交(因此有一个原子的"头"提交,为所有代码原子性地向前移动)
  • 统一的目录层次结构和所有源代码的单一视图
  • 检出或提交代码的单一位置(包括所有读取或写入内容的工具)
  • (有时)检出、提交和依赖的最小单位是文件
  • (通常)没有项目的概念,只有目录和文件的概念
  • (有时)单一版本规则:任何时候仓库中任何依赖只能有一个版本
  • 要求库维护者解决他们引起的问题的能力

跨项目的原子提交

假设我们有两个独立的项目A和B。我们想要做一个影响这两个项目的更改。Monorepo的一部分是保证你可以同时原子性地提交到这两个项目。不存在项目A处于提交#1而项目B处于提交#2的仓库视图。

当你想要进行一个更改,如果项目A或B没有同时更改就会被破坏时,这一点尤其重要。例如,假设我们有一个名为App的项目,它依赖于一个名为Library的项目。我们想要更改Library中函数的签名并同时更新App。如果我们只更新Library或只更新App,那么App就会被破坏。

这个特性最依赖于事物是否在单个源代码仓库中,因为实际上"仓库"的定义是"你可以原子性地提交多个文件的位置,它跟踪这些原子提交,并且你可以从该原子提交历史中的任何点检出"。

这个特性还意味着整个仓库有一个单一的"头"(最新提交)定义。这一点很重要,因为当开发者从仓库检出时,他们通常在"头"处检出。这意味着当开发者检出时,无论他们同时检出多少项目,都能保证获得整个源代码树的一致视图。他们永远不必考虑是否检出了两个相互不兼容的App和Library版本。

标准化的跨项目目录结构

Monorepo中的所有代码都被认为是在单个目录结构中。这在开发时和浏览代码时都有优势。

开发期间:检出是标准化的

在开发期间,如果项目A存储在仓库中的/path/to/project/A,项目B存储在/path/to/project/B,当我同时检出它们时,它们将在彼此相邻的目录中。我可以保证这将是目录结构。如果我需要在开发时让它们协同工作,我永远不必考虑应该将项目A放在磁盘上的什么位置相对于项目B。

统一的代码浏览方式

由于你有单一的目录结构,在代码搜索工具中浏览目录相对简单,并且可以有一个搜索该单一仓库的单一代码搜索工具。

单一检出和提交位置

Monorepo的价值之一是不必思考"我应该从哪个仓库检出?“相反,开发者只需要考虑他们需要检出什么代码。类似地,所有提交都进入同一个仓库。

这也意味着你对整个历史中的所有提交有一个单一视图,这有时会很有帮助(例如当你试图找出时间A和时间B之间可能改变的所有内容以进行调试时)。

最后,所有工具只需要担心访问单个仓库——它们只需要关心目录和文件名。

文件是检出、提交和依赖的最小单位

在大多数Monorepo中,你可以提交的、由版本控制系统跟踪的最小事物是一个文件。系统知道"一个文件"是发生了什么变化。

在一些Monorepo中,你还可以在不检出整个仓库的情况下检出单个文件。事实上,如果仓库变得非常大,这就成为一个非常重要的生产力特性。

此外,在一些Monorepo中(特别是Google的),依赖的最小单位是一个文件。这意味着构建系统可以知道一个文件依赖于另一个文件。

没有项目的概念

由于所有内容都在同一个仓库中,没有固有的概念认为不同目录的集合都可以代表单个"项目”。构建系统可能知道某些目录被编译在一起以产生特定的工件,但没有通用的方法可以通过查看目录结构或类似的东西来轻松看到这一点。

单一版本规则

通常,Monorepo会强制规定任何给定软件在仓库中同时只能存在一个版本。如果你签入一个库,在整个仓库中你可能只能签入该库的一个版本。由于你有一个Monorepo,这最终意味着在公司中任何时候该库只能存在一个版本。

这样做有多个原因。

首先,它使得推理系统行为更加容易。你总是理解你将获得依赖的哪个版本。

但也许这样做的最重要原因是大多数编程语言强制规定最终程序中任何特定依赖只能存在一个版本。

这个规则也有一些相当显著的缺点。如果你拥有很多人依赖的一段代码,升级那段代码可能非常困难,因为你所做的任何更改都会破坏某人。

当涉及到第三方库时,这真正成为一个问题。如果所有代码必须存在于你的仓库中,这意味着你必须将第三方库签入你的仓库。而且对于公司中的每个人来说,它们只能有一个版本。

让库维护者解决他们引起的问题

在Monorepo世界中,如果你拥有一个库,你可以通过签入与那些项目不兼容的内容来破坏每个依赖你的项目的构建。在单一版本世界中尤其如此,库所有者必须签入每个人都依赖的库的单一版本。

这意味着库维护者不能仅仅强制他们的消费者完成升级到库新版本的所有工作。库维护者必须深入并自己完成工作。

总结

所以你可以看到,“Monorepo"实际上远不止是把你所有的东西放在一个源代码仓库中。有些人已经将所有这些事物分组在一起,因为上面基本上是对Google Monorepo的描述,大多数人在谈论"Monorepo"时似乎都在考虑那个系统。但分离这些概念很重要,因为它们中的许多可以在你今天拥有的系统中实现。此外,也许并非所有这些事物实际上都是好的,也许你应该有意识地决定在你的业务中尝试采用哪些。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计