本文注重探讨什么情况下适用 Monorepo 方案,即 why,找教程想看 how 的直接关了吧
什么是 Monorepo
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。由于已经有很多各种 Monorepo 实践的介绍,我这次换一个角度,探讨一下什么情况下使用 Monorepo 更合适。
历史
了解 Monorepo 的定义之后,让我们先来看一下它的历史。在过去的几年中,出现了一些工具和框架来支持 Monorepo 的实践。
Lerna
一句话:Lerna 解决了多个包互相依赖的开发、发布问题
Lerna 是最早的第一代开源的 Monorepo 方案,由 Babel 开发维护,最擅长管理依赖关系和发布。Lerna 优化了多包工作流,解决了多包依赖、发版手动维护版本等问题。Lerna 的功能很集中,也不提供构建、测试等任务,工程能力较弱,在现在的使用项目中往往需要基于它进行顶层能力的封装。
Lerna 解决了三个多包开发中的痛点问题:
- 代码共享,调试便捷:一个依赖包更新,其他依赖此包的包/项目无需安装最新版本,因为 Lerna 自动 Link
- 安装依赖,减少冗余:多个包都使用相同版本的依赖包时,Lerna 优先将依赖包安装在根目录
- 规范版本管理:Lerna 通过 Git 检测代码变动,自动发版、更新版本号
Nx
一句话:Nx 是 Lerna 之后的第二代大而全 Monorepo 解决方案,所以 Nx 很重。
Nx 是 Nrwl 团队开发的,目前这个团队已经接手了 Lerna 的维护,所以以后 Lerna 和 Nx 实际上是一家了。Nx 提供了一套工具和库,可以帮助开发人员更好地组织和管理他们的代码。Nx 的命令行工具可以快速的生成不同用途模块的模板代码,并且接入到 Monorepo 中。Nx 通过缓存、增量构建、并行构建等机制,优化、加速了构建流程。但是由于 Nx 的使用成本比较高,本地构建、CICD 等都需要接入 Nx 的体系,导致对于已有项目的侵入性比较高,所以在国内的使用相对较少,没有技术包袱且对大而全方案有兴趣的人可以选择 Nx。
Pnpm
一句话:Pnpm 作为目前最流行的替代 npm 和 yarn 的包管理器,通过 workspace 轻量化实现了 Monorepo 的功能,并且有很好的扩展定制能力。
使用 pnpm 的 Monorepo 解决方案,你可以在一个仓库里维护多个包,通过 pnpm 进行依赖的安装、链接。 pnpm 使用硬链接和符号链接的方式,提供了各工程间依赖的管理,可以有效地支持 Monorepo 的使用。作为 Monorepo 方案,pnpm 通过其特有的链接管理依赖的方式,解决了 Monorepo 中幽灵依赖的问题,这个问题的具体情况会在后面再聊。
此外 pnpm 相比于 npm、yarn 的包管理器,还有下面的优势:
- 装包速度快:缓存中有的依赖,直接硬链接到项目的 node_module 中;减少了 copy 的大量 IO 操作
- 磁盘利用率极高: 软/硬链接方式,同一版本的依赖共用一个磁盘空间;不同版本依赖,只额外存储 diff 内容
问题
之所以把问题放在前面说,因为任何技术都有 trade off,只有接受了一项技术的限制,才能充分发挥这项技术的优势。
幽灵依赖
npm/yarn 安装依赖时,存在依赖提升,某个项目使用的依赖,并没有在其 package.json 中声明,也可以直接使用,这种现象称之为 "幽灵依赖"。随着项目迭代,这个依赖不再被其他项目使用,不再被安装,使用幽灵依赖的项目,会因为无法找到依赖而报错。pnpm 的 Monorepo 可以彻底解决这个问题。
依赖安装、构建打包耗时长
Monorepo 中每个项目都有自己的 package.json 依赖列表,随着 Monorepo 中依赖总数的增长,每次 install 时,耗时变长。在多个项目构建任务存在依赖时,如果是串行构建或全量构建,伴随依赖安装耗时长的,构建时间也会变长。不过这些问题目前也都有了解决方案,可以直接使用工程化程度高的大而全方案,如 Nx,也可以基于 pnpm 的方案自己再进行构建优化。
编译部署、CICD
因为 Monorepo 中存在多个项目,所以编译部署较单个项目的 Multirepo 方案要复杂一些。这里由于 CICD 方案不同,所以在这里不进行展开,各位看官明白这里可能是个坑即可。
隔离解耦
我认为这是 Monorepo 方案最大的坑点,但市面上大部分的文章或分享都较少提及。这不是一个单靠技术能解决的问题。在开发 Monorepo 的项目时,项目越靠近底层需要被多个项目复用,代码腐化的破坏力越强,越要小心。最好遵循如 SOLID 这样的软件开发原则,避免包之间产生网状依赖,尽量保证每个包可以独立编译工作,否则很容易快速堆积出无法维护的巨型屎山。
团队协作
这其实是 Monorepo 的前置条件,因为 Monorepo 使用一个代码库管理多个项目,所以必然会造成团队成员可以访问到与自己无关的代码。使用 Monorepo 也就意味着接受了使用团队间的代码共享,如果对代码隔离有强要求,Monorepo 的方案可能就不合适了。此外,一套合适的团队协作流程也可以帮助 Monorepo 的代码库更好发展。
作用
从 Monorepo 方案的历史演进可以看出 Monorepo 最主要就是为了解决复用的问题,并且不断优化复用过程中的开发体验,解决出现的问题。对于现代的前端项目来说,复用主要可以分成 3 个方面 ------ 模块代码、基础设施、以及构建部署。
组织业务项目
技术为业务服务,Monorepo 也不例外,解决模块代码的复用是 Monorepo 初衷。从帮助开发者管理依赖包的版本,到实现分包但不发包的 npm 复用方式,现代的 Monorepo 方案已经可以非常方便的实现模块复用。业务项目中有需要复用的模块,可以将多个业务项目用 Monorepo 的方式进行组织,再把复用模块拆分成单独的包引入多个项目中,并且根据需要决定和业务项目的配合方式,是否需要单独发包等。
统一基础设施,提高工程化程度
现代前端项目一般都有基于 Webpack 和 Node 的基础设施。虽然基础设施的共享可以通过模板、CLI 工具等其他方式实现,但随着时间的推移,老项目的基础设施难免跟不上团队技术栈的迭代,老项目也就逐渐变成技术债。Monorepo 通过共享所有项目的基础设施和工具链,各子项目在此基础上再根据自己的需要定制化。像 Nx 这种大而全的工程化方案,还可以快速升级、管理项目的基础设施,抹平项目间的差异,避免技术债的产生。
依赖管理
依赖管理是 Monorepo 的诞生原因之一,它不但可以统一管理的包中发布的共同依赖的版本,减少安装的相同依赖,便捷的进行升级。也可以让开发者从各种 npm link 中解放出来,快速添加自己的编写的其他私有包到依赖中。pnpm 的方案也解决了幽灵依赖的问题,让 Monorepo 中项目的依赖关系更加安全。此外,很多工程化方案还增加了缓存,加速 Monorepo 中项目的安装、构建速度,让开发者获得比 Multirepo 更好的体验。
总结
我认为目前的 Monorepo 方案更适合配合较为紧密的团队,用在整体规模不太大的需求中。Monorepo 相比于传统的发包方案,可以更便捷的复用各种模块,并且用很小的成本保证多个项目的技术栈统一。但同时,由于模块会被多个项目复用,Monorepo 对于开发者提出了更高的要求,如果没有做好模块间的隔离解耦,更容易写出灾难性的代码库。对于那种手里有锤子,看什么都是钉子的氛围,Monorepo 很可能带来更大的灾难。一切的涉及协作的技术选择都取决于团队的特性和业务的要求,Monorepo 在一定条件下,可能会是你的代码管理的最佳选择。