写在最前
本故事简要地介绍了 Monorepo 的 What 和 Why,重点篇幅在于搭建一个好用的 Monorepo 工程时应该考虑的点。可以作为你在选择工具时的条件,也可以作为你在搭建 Monorepo 工程时查漏补缺的参考。希望这对你有所帮助,哪怕只是一点点 \^O^
“在这个 AI 内容生成泛滥的时代,依然有一批人"傻傻"坚持原创,如果您能读到最后,还请点赞或收藏或关注支持下我呗,感谢 ( ̄︶ ̄)↗”
What?- 独立和关系
丹尼尔:蛋兄,好久不见,今天我们来聊聊 Monorepo 吧!
蛋先生:Monorepo?就是把多个项目放在同一个仓库里的那种吗?
丹尼尔:是呀,感觉上就是把一堆代码库简单地堆在一起
蛋先生:你说得不太准确,我觉得 Monorepo 最重要的是俩关键字:独立和关系
丹尼尔:怎么说?
蛋先生:独立是指这些项目本身是完整的,一般都拥有开发、测试,发布等完整的生命周期,而不是简单的包含一堆代码文件的文件夹
丹尼尔:哦,这个我明白了。那关系呢?
蛋先生:关系是指这些项目之间存在一定的关联,比如它们属于同一个业务领域,或是有依赖关系,而不是毫无关联地硬堆在一起
丹尼尔:懂了!
Why?- 更好地协作
丹尼尔:那用这个 Monorepo 有什么好处呢?我以前一个项目一个仓库不也挺好的吗?
蛋先生:这里科普一下,一个项目一个仓库有一个专用的名词叫 Polyrepo。我认为 Monorepo 最关键的好处在于项目与项目之间的协作
丹尼尔:怎么说呢?
蛋先生:比如共享代码以减少重复工作方面。当你在开发应用 B 时,如果发现应用 A 中已经实现了很多相似的逻辑,那么你需要把共享逻辑抽取到一个独立的库 α,然后修改应用 A 和应用 B 以依赖于库 α,因为这一切都在同一个仓库中完成,非常方便,操作成本较低
丹尼尔:确实,如果采用 Polyrepo 的方式,我得新建一个仓库,把共享逻辑抽取出来,然后通过本地 link 的方式来开发调试。一切就绪后,还得发布到 npm,再在应用 A 和应用 B 中安装依赖。而且每次修改都需要重复这个过程,真是麻烦
蛋先生:再比如库修改可能导致项目不稳定方面。当一个被依赖的库进行迭代升级时,特别是有大的变更时,如果没有及时沟通以采取相应的措施,就会导致各种问题,潜在的风险非常大
丹尼尔:Monorepo 不会有这个问题吗?
蛋先生:在 Monorepo 中修改是原子的,即当你修改库 α 时,同仓库的应用 A 和应用 B 都能及时感知到变更。例如,你删除了某个接口的入参参数,应用 A 和应用 B 会立刻报错,这样就能及时发现并解决潜在风险
丹尼尔:这样确实挺棒的
蛋先生:最根本的原因是 Polyrepo 带来了隔离,而隔离影响了协作。Monorepo 的目标则是为了更好地协作。就像部门间协作和部门内协作,显然同一个部门内的协作效率更高,沟通成本也更低
丹尼尔:一语中的!
How?- 舒适地开发
➥ 初始化阶段 - 脚手架
丹尼尔:那采用 Monorepo 的形式来组织项目,我应该怎么做呢?
蛋先生:我们一起来走一走应用开发的历程,看看需要有哪些工作吧
丹尼尔:好啊
蛋先生:有两种开局方式。一种是全新开始,这样的话你需要一个能生成 Monorepo 大仓的脚手架
丹尼尔:恩,很体贴
蛋先生:不过这种情况发生的概率较低,通常是一次性的。更常见的是在已有的 Monorepo 仓库中增加新项目。这是经常需要做的事情,所以我们可以提供多种脚手架代码生成器来快速初始化一个项目,比如创建 TS 工具库项目、React 应用项目,或者是 TS CLI 项目等等
丹尼尔:确实,常用的项目类型是可以枚举出来的。有了这些工具,后续增加项目就轻松多了,想想就很爽!那另一种开局呢?
➥ 初始化阶段 - 依赖安装
蛋先生:另一种开局是你准备在一个已存在的 Monorepo 大仓上进行开发工作。这时,你的第一件事应该是安装依赖,对吗?
丹尼尔:恩,没错
蛋先生:不过,大仓里可能有很多项目,你总不能一个一个项目进行安装依赖吧,所以需要有一个可以一次性安装全部项目依赖的能力
丹尼尔:对啊,我可不想把时间浪费在一个个项目里 cd 来 cd 去的
➥ 开发阶段 - 任务编排
蛋先生:无论哪种开局,接下来都是进入到开发阶段了。假设你在开发应用 A,而应用 A 依赖库 α,那么你是不是得先确保库 α 有可用的构建产物?
丹尼尔:是啊,所以第一步就是得知道应用 A 依赖了哪些同仓库中的其他库,并且提前对它们进行构建。但如果依赖关系比较复杂,就难搞了
蛋先生:正是如此。所以,我们希望能够不用手动处理这些依赖,只要对应用 A 进行构建,就能自动处理它所依赖的所有库的构建
丹尼尔:那就太好了!
蛋先生:这就需要任务编排了。我们可以配置任务之间的协作关系,比如在执行某个任务之前,需要先执行哪些任务,这些任务是串行还是并行执行等等
丹尼尔:哦,任务编排还真好用
➥ 开发阶段 - 一致命令
蛋先生:好了,万事俱备,你可以开始本地开发调试了
丹尼尔:哦,那我先看看项目的 README,找找本地开发调试的指引
蛋先生:不用那么麻烦,直接执行 dev 命令吧。无论你是在开发应用项目还是库项目,无论是用 JavaScript 还是 Java,开发就运行 dev,构建就运行 build,测试就运行 test,等等。这样你就不会有任何心智负担
丹尼尔:哈哈,老早就想这样了
➥ 开发阶段 - 影响检测
蛋先生:开发过程中,你发现依赖的库 α 提供的接口有点小问题,现在你准备对应用 A 所依赖的库 α 进行修改
丹尼尔:哦,反正都是在同一个仓库,修改起来挺方便的
蛋先生:但我们得确保这个改动不会影响到依赖该库的其他项目。至少在我们可控的范围内,比如同一仓库中依赖该库的其他项目。所以,我们需要一种自动检测机制来识别哪些项目受到了影响,然后对这些受影响的项目进行单元测试等操作,以确保它们的稳定性
➥ 开发阶段 - 依赖分析
丹尼尔:蛋兄果然很谨慎啊
蛋先生:咳咳~。其实,这一切都需要借助依赖分析能力。当 Monorepo 的规模越来越大时,依赖关系也会变得越来越复杂。我们需要通过依赖关系图,清晰地了解各项目之间的联系和影响,从而做到对项目状况了如指掌
➥ 开发阶段 - 依赖权限
蛋先生:你现在是库 α 的主要负责人。有一天,你发现了一些并不想对外暴露的 API 被仓库内的其他项目使用,结果你在修改这些 API 时就不得不考虑对这些项目的影响
丹尼尔:啊,虽然我是声明了 export,但这只是为了库内部的其他代码使用。可其他项目却可以通过深层导入来依赖这些 API
蛋先生:嗯,所以我们需要在工程层面上建立机制,防止这些 API 被误依赖
➥ 开发阶段 - 修改权限
蛋先生:库 α 虽说是由你主要负责的,但是由于代码库是放在一起的,其他拥有大仓权限的同学也就有权限进行修改。但是你并不希望他们随意修改库 α 的代码,至少要经过你的同意
丹尼尔:是啊是啊,这真的很重要!
蛋先生:所以我们需要引入类似 OWNER 的机制,对这些修改权限进行限制,以确保代码的稳定性和一致性
➥ CI 阶段 - 本地计算缓存
“注:CI 阶段的能力,不仅仅只用于 CI,开发阶段也是可以享用,只是为了剧情需要这么安排而已”
蛋先生:好了,项目修改完毕,提交。CI 开始工作了,然后你发现每次 CI 构建都非常慢
丹尼尔:嗯,我加点戏哈。我喝了一杯咖啡,再回来一看,好家伙,CI 还在跑。这样可不行,得优化性能了,不然我快要崩溃了
蛋先生:好吧,这戏加得... 回到正题。这是因为该项目直接或间接依赖了同一仓库中的好几个其他库。所以,每次构建实际上都需要构建多个项目。优化性能的思路之一就是减少不必要的计算,增量执行就变得非常重要。因此,我们需要引入本地计算缓存,缓存计算结果,避免对没有修改的库进行重复构建
丹尼尔:本地缓存,我懂
➥ CI 阶段 - 分布式任务执行
蛋先生:性能优化的另一个思路是加速必要的计算
丹尼尔:昨加速捏?
蛋先生:可以采用分布式任务执行。将一些可以并发执行的任务分配到不同的服务器上并行处理,实现在更短的时间内完成任务。这样做虽然会增加一定的成本,但对于大型项目来说,是非常有效的性能提升方案
丹尼尔:听上去好高级的样子
➥ CI 阶段 - 远程计算缓存
蛋先生:虽然使用了本地缓存,但每个服务器都需要先构建一次才能生成本地缓存。如果我们把缓存的位置移到远程云端,是不是就可以进一步优化性能呢?
丹尼尔:Nice! 这样就可以共享缓存了
➥ 发布阶段
蛋先生:最后,我们需要把库 α 发布到 npm 上去,因为它提供的功能非常通用,不仅仅局限于当前的项目仓库内
丹尼尔:那就赶紧发布吧!
蛋先生:发布阶段,根据需要,利用任务编排就可以了
新的问题
丹尼尔:听起来 Monorepo 灰常好啊,都使用这种方式得了
蛋先生:Monorepo 确实突破了 Polyrepo 的隔离问题,但这样开放的结构也带来了读权限的问题。如果你的大仓中的部分项目需要由第三方团队来开发,但你又不希望他们能看到其它项目的内容,那么 Monorepo 就无法解决这个问题了
丹尼尔:啊,那怎么办呢?
蛋先生:这种情况下,你可以考虑将这些项目作为 git submodule 分离出去。这样一来,大仓中的其它项目仍然可以在工作空间内直接依赖这些分离出去的 git submodule 项目
丹尼尔:那有啥需要注意的地方吗?
蛋先生:要注意的是,git submodule 的项目就不能通过工作空间直接依赖大仓中的其它项目了,它们需要通过 npm 中央仓库来进行依赖管理
丹尼尔:好咧,这一聊,天色已晚
蛋先生:嗯,今天就先聊到这里,就此别过吧
丹尼尔:拜拜!
写在最后
为什么不直接写一个使用某个工具(比如 Turborepo)来搭建 Monorepo 项目的教程呢?因为我相信聪明的你,只需要阅读官方文档,就可以轻松上手了
不同工具工作方式有所不同,但都是围绕 Monorepo 来提供能力的。我们应以不变应万变,掌握表面之下的东西,这样才能更加灵活地应对各种变化
“亲们,都到这了,要不,点赞或收藏或关注支持下我呗 o( ̄▽ ̄)d”