俯瞰 Monorepo,别一番风景!

简介: 本故事简要地介绍了 Monorepo 的 What 和 Why,重点篇幅在于搭建一个好用的 Monorepo 工程时应该考虑的点。可以作为你在选择工具时的条件,也可以作为你在搭建 Monorepo 工程时查漏补缺的参考。希望这对你有所帮助,哪怕只是一点点 \^O^

image

写在最前

本故事简要地介绍了 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”

目录
相关文章
|
1月前
|
安全 物联网
零压力了解 LoRA 微调原理
`LoRA` 全称为 `Low-Rank Adaptation`,翻译成中文就是`低秩适配`。⊙﹏⊙ 是不是一头雾水?没关系,相信我,看完下文你就会明白个大概了
231 0
零压力了解 LoRA 微调原理
|
7月前
|
Kubernetes Java 开发工具
CI/CD(五)Flink 应用部署
Flink 应用需要解决的是任务的灵活增加(通常以 maven module 的方式存在同一个git仓库中),不能依赖手工注册应用或.polaris-ci.yml自动注册
204 2
|
存储 缓存 资源调度
Monorepo(单体仓库)与MultiRepo(多仓库): Monorepo 单体仓库开发策略与实践指南
Monorepo(单体仓库)与MultiRepo(多仓库): Monorepo 单体仓库开发策略与实践指南
991 0
|
JSON 数据格式 Python
Python快速获取国内最新放假安排数据
Python快速获取国内最新放假安排数据
222 4
|
SQL 关系型数据库 MySQL
MySQL语法
MySQL语法
258 4
|
存储 Rust 前端开发
Tauri 开发实践 — Tauri 配置介绍
本文首发于微信公众号“前端徐徐”,主要讲解`package.json`、`Cargo.toml`及`tauri.conf.json`三个文件的配置。其中,`tauri.conf.json`最为复杂,涉及众多配置项。`package.json`用于配置前端依赖与脚本;`Cargo.toml`用于声明Rust应用依赖;`tauri.conf.json`则管理前端资源、API白名单等。这些配置对于Tauri应用的开发至关重要。
657 5
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:实现过程与关键细节解析an3.021-6232.com
随着互联网技术的快速发展,ASP.NET作为一种广泛使用的服务器端开发技术,其与数据库的交互操作成为了应用开发中的重要环节。本文将详细介绍在ASP.NET中如何连接SQL数据库,包括连接的基本概念、实现步骤、关键代码示例以及常见问题的解决方案。由于篇幅限制,本文不能保证达到完整的2000字,但会确保
|
存储 JSON 安全
在项目中到底应不应该用jwt?
JSON Web Tokens(JWT)是一种开放标准,用于在网络上传输安全信息。它常用于身份验证场景,用户登录后,服务器生成JWT并返回给客户端。客户端在后续请求中携带此令牌,服务器验证其有效性来确认用户身份。JWT具有无状态、可扩展和安全的特点,支持跨域认证,但也有令牌大小、续期复杂等缺点。是否使用JWT取决于项目需求,多数公司在采用,除非有特殊理由避免。以下是Go语言中使用JWT的一个示例。
325 0
|
存储 索引 Python
Python中,无序列表(Unordered List)
Python中,无序列表(Unordered List)
740 1
|
机器学习/深度学习 人工智能 算法
智能AI机器人管家
应用场景:四层别墅,每层面积1000平+,有自己的地下停车场,地下酒窖,阳光房,花园,广场。在此场景下想做一个智能机器人管家。 主要功能: 1、自动跟随制定的人,可以根据设定的人的语音指令做一些动作,比如:停下、充电、倒退、去某个地方等等。 2、可以根据设定的人的语音回答一些常见问题,比如:今天天气,股票走势,黄金走势,钢铁走势等能够从网上直接获取到的信息。 3、可以根据设定的人的语音回答一些本地问题,比如家里还有多少酒,调一下某地的监控,花房的温度多少,打开某房间的空调。 4、可以帮助做一些本企业决策性的问题。