原文:manav - 2024.10.29
九个月前,我们切换到了 monorepo。在此,我将介绍我们迄今为止的切换经验。
这并不是一份规范性的建议,而是一个经验的分享,目的是希望能够帮助其他团队做出明智的决策。
与大多数岔路不同,我们走过了两条路。因此,我会先描述导致我们改变的历史,概述我们在类似的情况中已经体验过的非 monorepo 方案,并因此能够更好地进行对比。
平台与 monorepo
Ente 的诞生可以追溯到五年前。它原本是一个端到端加密平台,用于存储 Vishnu 的所有个人数据,但后来发生了两件事: Vishnu 意识到需要这样一个平台的不仅仅是他自己,他还意识到要实现他的愿景需要做大量的工作。
于是,他从一个人变成了一个团队,并且不再试图处理所有的个人数据,而是将重点转移到其中的一个方面:Ente Photos,以此为起点让“飞船”起飞。对外界观察者来说,这似乎只是一个照片应用(实际上这确实是我们当前的具体目标),但背后驱动这一切的是我们对人类隐私权的坚持,即所有形式的个人数据都应受到保护。
我为什么要描述这些?因为从这个愿景来看,Ente 不是一个单一的应用程序,而是一个平台,将其代码存储在一个 monorepo 中是一个符合理念的选择。
这类似于 Linux 内核。大多数人不了解的是,按多项可量化指标衡量,全球最大的开源项目——Linux 内核本身——也是一个 monorepo。尽管它被称为“内核”,但实际上它是一整个平台,包括设备驱动程序等,代码组织为 monorepo 正是这种理念的体现。
坚持将 Ente 视为一个平台不仅仅是理念上的选择,它也带来了实际的益处。
例如,几年前,我们意识到还没有一款具有云备份功能的开源端到端加密 OTP 应用程序。于是我们为了自己的使用而构建了一款,因为是基于我们为照片应用创建的基础设施,实现并不难。
如今,这个副产品已成为世界上最受欢迎的具有上述特性的 OTP 应用。这看似是个意外,但其实不然,我们的计划一直是这样的:先建立一个稳固的平台,然后逐一处理我们需要的各种定制应用程序,以便更好地处理不同形式的数据。
微型代码仓库(Microrepos)
从理念上讲,Ente 作为一个 monorepo 是最合适的选择。但由于产品演变的历史因素,最开始并不是这样。硬件设备转变为软件,服务器组件在我们有能力进行审计之前是闭源的。像 Auth 这样的周末项目超出了它们的初衷,等等。
让我们倒带回两年前(仅仅为了选择一个大致对称的时间点)。虽然我们在包括开发人员数量在内的所有产品方面都在增长,但我们在增加工程人员方面非常谨慎,所以开发人员的数量并没有增加太多。因此,差不多是同样数量的开发人员在处理相同数量的产品(Ente Photos、Ente Auth),并同时支持多个平台(移动端、网页端、桌面端、服务器端、CLI)。
两年前,这些代码库分散在十几个仓库中。
到了今年二月,我们决定花时间完成服务器端开源的任务。这是一个很自然的时机来控制代码库的分散,于是我们借此机会切换到了 monorepo。
因此,作为一个规模相似的团队,做着类似的工作,我们已经体验了约一年分散的微型仓库设置,以及约一年集成的 monorepo 设置。
总结
如果要总结区别的话:切换到 monorepo 后,变化不大,而细微的变化都是正面的。
我们对此并不感到意外。其中大多数人对代码仓库的组织方式并不十分在意,总体上对这种改变也没有太高的期望。大家的整体感觉是 monorepo 可能会更好,所以为什么不试试呢?既然没有人反对这个选择,我们就这么做了,但我们并没有试图通过这次改变“解决”什么问题。
事实上,整体上变化不大。我们依然对开发速度感到满意,所以它并没有拖慢我们。然而,确实有许多小的改进,所以接下来的部分我将深入探讨这些改进。
更少的重复劳动
这是最大的实际收益。我们需要做的重复劳动大大减少了。
举个例子,考虑以下的 pull request。它修改了用于计算设备上人脸嵌入的机器学习模型。
这个更改影响了(1)照片移动端应用,(2)照片桌面端应用,(3)照片网页端应用,以及(4)机器学习的基础代码。
在之前的分仓库模式下,这将是四个不同的 pull request,分别提交到四个不同的仓库中,并且需要通过评论将它们联系在一起以供日后参考。
现在,这是一份 pull request。容易审查,容易合并,容易回滚。
更少的子模块
子模块是一个让人恼火但确实有效的解决方案。问题是真实存在的,因此需要解决方案,而子模块确实是一个合适的解决方案,但它们仍然令人恼火。
这就是说,我们感谢 git 子模块的存在,它是解决实际代码组织问题的一种方法,但我们希望不需要使用它们。
Monorepo 减少了那些本应需要子模块的地方,因此这也是一个优势。
举个例子,之前 Ente Photos 的网页端和桌面端代码库之间是子模块关系。每次需要发布或推动重要的更改到主分支时,都会涉及到繁琐的 PR 操作。现在这些都不需要了。这两个相互依赖的代码现在可以在同一个提交中直接引用彼此,变更可以原子性地完成。
更多的 Stars
这是最大的营销收益。之前我们的 Stars 分散在十几个仓库中。如果每个仓库有一千个 Stars,我们总共有 12k Stars,但由于人的心理和 GitHub 推荐算法的工作方式,这远不如一个拥有 12k Stars 的单一仓库来的有影响力。
简单
我们在切换时的一个顾虑是,这可能会影响开发速度。我们以为会需要发明各种机制和约定来避免互相干扰。
但这些顾虑被证明是多余的。我们没有发明任何东西,只是静观其变,结果并不需要任何新方案。因此,对个人开发者来说,这次切换是轻松的,因为我们没有要求团队的任何成员改变他们的工作流程。
目前为止,也没有“仓库范围”的指导原则,除了两个:
- 不要有仓库范围的指导原则
- 不要动根目录文件夹
就是这样。每个文件夹内,或者每个子团队内部,可以自由选择任何组织方式、编码约定等等。
我意识到,这种轻松对我们来说,可能是由于团队规模较小,以及我们对彼此能力的高度信任。而这两个因素可能无法在其他团队中复制。
长期重构
跨仓库的重构需要比在同一个仓库内的重构更多的勇气。技术上来说,两者没有区别,但心理上的障碍却有所不同。
举个例子,我们已经将许多不同的网页应用合并到一个类似的设置中,而无需事先制定详细的计划。这一切都很自然地发生了,因为我们能够看到它们“彼此相邻”,代码复用的机会变得显而易见。
连接感
这种“在共享空间中工作但不在同一文件夹中工作”的方式,让我们比起以前单独或者以子团队的形式在各自的仓库提交代码时,感觉更紧密相连。
之前,很容易沉浸在各自的工作中(这是好事),但有时也会让人觉得自己只是在处理一个小部分,而无法看到整体(这不是好事)。
现在,大家仍然可以沉浸在自己的“文件夹”中,保留了这种沉浸感的好处。但也有额外的微妙提示让我们看到自己的工作是如何与整体相互关联的。因此,这是一种双赢的局面。
我所描述的可能有些抽象,所以让我举个例子。每当执行 git pull
时,会看到团队成员正在处理的所有变更。最近更改的文件名,文件中的更改数,最近的分支名,最近推送的标签。这些单独来看都是低信息量且不精确的信息载体,我甚至不会有意识地去看它们。
但随着时间的推移,我发现这些“环境提示”无意识地、自动地让我对周围发生的事情有了极好的感知。哪些功能正在开发,完成的阶段如何,哪些 bug 修复被推送了,最近发布了哪些版本。
类似的偶然信息交换也会发生在我打开 pull request 页面时,我会不经意间瞥见其他人正在处理的内容。
最棒的是,这一切都是颠覆性的、毫不费力的。每个人都在做自己的事,而仅仅因为大家都在这个共享的数字空间里工作,就自然地产生了一种意识和连接感。
总结
这篇文章已经很长了,远超我原本的预期,所以就此打住。
我本可以提供一些建议,但我认为没有什么特别的技术诀窍是必须的。在切换之前让我感到困扰的一个问题是我们将如何管理 GitHub 工作流,但事实证明这很简单,因为我们可以将 GitHub 工作流的范围限定为仅在特定文件夹的更改上运行。
从工程师的角度来看,回顾性文档如果没有“优缺点”部分是不完整的,但到目前为止,还没有发现任何对我们有影响的缺点,所以请原谅这部分内容的缺失。
从个人角度来说,我最喜欢 monorepo 的是,它让我感到自己像是“巨轮”的一部分,这艘巨轮正在无情地驶向完美,并且已经获得了不可阻挡的势头。我现在写的代码不再是一个孤立的 Web 组件,或一个 goroutine,或一个小的文档修复,而是这个单一平台的一部分——一个将超越我生命的平台。