前言
在现代软件开发中,随着项目复杂度增加和多项目、多库需求的涌现,传统的单一代码库难以应对这些挑战。Monorepo(单一仓库)因其统一管理多个项目、库或服务的能力,成为解决复杂开发问题的重要工具。
本文将探讨团队在项目瓶颈下,从 Multirepo 迁移到 Monorepo 的实际经验,并分享 2024 年主流 Monorepo 工具的选择与应用实践,为团队提供迁移或实施 Monorepo 的实用指南。
Multirepo vs Monorepo
我们团队的 H5 项目最初采用 Multirepo 架构,各模块独立存储在不同代码仓库中。虽然在项目早期便于管理,但随着规模扩展,协同开发难度加大,版本控制和依赖管理逐渐暴露问题。
尤其是在多个模块间公共逻辑增多的情况下,各模块独立管理导致重复开发和维护困难。例如,为了实现模块间的数据共享和功能协作,我们尝试抽离公共逻辑,但分散的代码仓库让变更传播和依赖同步变得复杂,进一步加剧了构建和部署流程的不一致。
为了简化开发流程并提升团队效率,我们决定从 Multirepo 迁移至 Monorepo 架构。通过将所有模块统一存储在单一代码仓库中,我们期望降低协作成本,实现更高效的版本管理和更一致的开发体验。接下来,我们将深入探讨 Monorepo 工具的选型过程及其在 H5 项目中的实践应用。
常用的 Muntirepo 工具
在采用 Monorepo(单一仓库)架构的软件开发中,工具的选择是至关重要的。合适的 Monorepo 工具能够帮助团队更高效地管理大规模代码库、提升协同开发体验以及优化构建和部署流程。
直至 2024 年,目前在前端界比较流行的 Monorepo 工具有 Rush、Turborepo、Lerna、Yarn Workspaces、Pnpm Workspaces、Yalc、npm Workspaces 和 Nx,下面将会简单分享各个热门工具的优缺点。
Rush
Rush 是由 Microsoft 打造的 Monorepo 管理工具,旨在优化大型项目的构建和开发流程。它支持同时管理多个包,提供自动版本管理、并行构建、增量构建等功能,通过统一的命令行工具简化 Monorepo 的操作。尽管具有完整的生态系统和强大的社区支持,但学习曲线可能较陡峭,适用于大型项目而对小型项目可能显得过于庞大。
- 优点:
- laoder主要将代码转换为浏览器能识别的原生js代码,本质就是个函数转换器
- 完整生态系统: Rush 提供了一系列工具和脚本,构建了一个完整的 Monorepo 生态系统,从版本管理到构建发布都有涵盖。
- 自动版本管理: 具备自动版本升级和依赖管理的功能,简化了维护过程,减轻了开发者的负担。
- 并行构建和增量构建: Rush 支持并行构建,提高了整体构建效率,同时也支持增量构建,仅构建发生变化的包,节省了构建时间。
- 社区支持: 作为由 Microsoft 支持的项目,Rush 有庞大的社区基础,保证了持续的更新和维护。
- 缺点:
- 学习曲线: Rush 提供了丰富的功能,学习曲线相对较陡峭,可能需要一定时间来熟悉和掌握。
- 适用性: 由于其强大的功能集,更适用于大型项目,对于小型项目来说可能显得过于庞大,不太合适。
Turborepo
Turborepo 是专注于提升大型 Monorepo 项目性能的工具,通过支持并行构建和增量构建等功能,显著减少了构建时间,同时具备分布式缓存和模块化工具链设计,为开发者提供了更灵活的定制选择。虽相对较新,但其性能优化和灵活性使其在大型项目中备受欢迎。
- 优点:
- 性能优化: Turborepo 专注于提升 Monorepo 项目的性能,通过并行构建和增量构建等手段,显著减少了构建时间。
- 定制灵活: 工具链模块化设计,使得开发者可以根据项目需求选择和配置不同的工具,提供更大的灵活性。
- 缺点:
- 较新项目: 相对于一些更成熟的 Monorepo 工具,Turborepo 可能相对较新,社区生态和文档可能相对有限。
- 学习曲线: 与其他 Monorepo 工具相比,Turborepo 的学习曲线可能较陡峭,需要一定时间的适应。
Lerna
Lerna 是一款专为管理具有多个软件包的 JavaScript 项目而设计的工具,旨在优化大型 Monorepo 项目的版本管理、依赖共享以及发布流程。通过提供一套强大的命令行工具,Lerna 简化了 Monorepo 项目的操作,使开发者能够更轻松地管理和维护庞大的代码库,提高开发效率和代码的可维护性。
- 优点:
- 版本一致性: Lerna 有助于确保 Monorepo 中各个软件包的版本保持一致,减少了因版本不一致而导致的问题。
- 依赖管理: 提供了方便的依赖管理机制,允许在项目中共享和重用代码,减少了重复劳动。
- 发布自动化: Lerna 支持自动化的发布过程,能够一次性发布所有更新的软件包,提高了发布的效率。
- 操作简便: 具备强大的命令行工具,使得 Monorepo 项目的操作变得简单易行,包括软件包的添加、删除、测试等。
- 缺点:
- 上手难度: 对于初学者来说,Lerna 的学习曲线可能较陡峭,需要花一些时间熟悉其概念和操作。
- 维护成本: 随着项目的规模增加,Monorepo 中的软件包数量可能庞大,这会增加维护的难度和成本。
- 配置复杂: 部分用户认为 Lerna 的配置相对复杂,需要仔细调整以满足特定项目的需求,可能需要更多的配置文件。
Yarn Workspaces
Yarn Workspaces 是 Yarn 包管理器的一项强大功能,专注于优化 Monorepo 项目的依赖关系管理。它允许将多个包组织在同一个版本控制存储库中,通过统一依赖版本解决了版本冲突问题,同时通过共享顶层 node_modules 目录,有效减小了磁盘占用。Yarn Workspaces 支持交叉包引用,提供了更灵活的项目组织方式,并通过并行安装加速了整体构建速度。它是一个轻量级而功能强大的 Monorepo 解决方案,尤其适用于中小型项目和对简单性要求较高的团队。
- 优点:
- 依赖一致性: 通过统一依赖版本,有效解决了依赖冲突和版本不一致的问题,提高了项目的稳定性。
- 资源共享: 通过单一 node_modules 目录,减小了磁盘占用,提高了模块查找效率。
- 灵活性: 允许在 Monorepo 中的包之间进行相互引用,提供更灵活的项目组织方式。
- 缺点:
- 初始学习曲线: 对于新用户来说,Yarn Workspaces 的使用可能需要一定时间的学习,特别是对于没有使用过 Yarn 的开发者。
- 功能相对简化: 与一些专注于 Monorepo 管理的工具相比,Yarn Workspaces 的功能相对简化,可能不适用于所有复杂的项目结构。
Pnpm Workspaces
pnpm workspace 是 pnpm 包管理工具的一个功能模块,专注于支持 Monorepo(单一仓库)的工作区管理。它通过高效的依赖共享机制,将多个相关的包集中管理,实现了更快速、更节省空间的依赖安装和执行。pnpm workspace 提供了命令行工具,支持多包管理、依赖共享、版本一致性维护、脚本运行等功能,使得在单一仓库中管理多个包变得更为便捷和高效。尽管可能存在学习曲线和部分生态支持的挑战,但其在 Monorepo 场景下的优势在于提供了快速、节省空间、版本一致性等方面的综合解决方案。
- 优点:
- 快速的安装和执行: pnpm 利用了硬链接和符号链接的方式,使得依赖的安装和执行更加迅速。
- 磁盘空间节省: 通过依赖共享机制,pnpm 节省了大量磁盘空间。
- 版本一致性: 工作区功能有助于保持包的版本一致性,降低了由于版本不一致导致的问题。
- 缺点:
- 学习曲线: 对于不熟悉 pnpm 和工作区概念的开发者来说,需要一些时间来适应和学习。
- 部分生态支持: 虽然 pnpm 在支持的生态方面不断增加,但与其他更成熟的包管理工具相比,仍有一些生态上的差距。
Nx
Nx是一个开源的工具,专为管理和开发大型 Monorepo 项目而设计。它建立在 Angular CLI 之上,提供了一套功能强大的工具和插件,支持多语言、多框架的项目。Nx 的核心理念是通过插件化的方式,为开发者提供更高层次的抽象,以提高项目的可维护性、可扩展性和开发效率。Nx 支持生成可重用的领域库、定义和执行一致的工作流,以及提供强大的可视化工具来监控项目和性能。
- 优点:
- 强大的插件生态: Nx 提供了丰富的插件生态系统,支持多语言、多框架,使得开发者可以选择最适合其项目的工具。
- 一致的工作流: Nx 引入了一致的工作流程,通过定义任务和脚本,提高了多包项目的开发效率和一致性。
- 全面的可视化工具: Nx 提供了全面的可视化工具,帮助开发者更好地理解项目结构、监控性能,并提供快速导航和跳转。
- 缺点:
- 学习曲线较陡峭: 由于 Nx 提供了丰富的功能和抽象,初学者可能需要花费一些时间来理解和熟悉其工作原理。
- 依赖于 Angular: Nx 的核心建立在 Angular CLI 之上,因此对于不使用 Angular 框架的项目可能显得过于重量级。
- 复杂性增加: 尽管 Nx 提供了丰富的功能,但对于小型项目而言,引入 Nx 可能带来不必要的复杂性。
对比分析
各 Monorepo 工具的统计数据来源于 stateofjs.com 网站, 由于 2023 年数据暂未出炉,截止数据为 2022 年底。需要注意的是,数据的来源样本可能会一定程度影响整体数据分析,因此这些统计数据仅供参考,不代表绝对性。同时,一些流行度较低的工具可能未被纳入统计。
Monorepo 中的依赖管理
在全面评估各个 Monorepo 工具的优劣势,并结合团队实力、认知度、使用度、关注度以及满意度等多方面因素,我们初步确定了 Lerna、Yarn Workspaces 和 Pnpm Workspaces 这三个工具用于企业级 Monorepo 项目管理。
然而,通过查阅 Lerna 官方文档的Legacy Package Management章节,我们了解到它将不再负责安装和链接项目中的依赖项,而将这一任务交由更优秀的包管理器,如npm、yarn和pnpm来处理。
因此,在工具选型时,我们需考虑当前工具的可升级性和可扩展性,所以我们决定放弃使用 Lerna 进行项目间的依赖管理。
接下来,我们将聚焦于比较 Yarn Workspaces 和 Pnpm Workspaces 这两个工具。它们在处理项目间的依赖管理方面表现优异且具有较好的兼容性。然而,在依赖安装方面存在细微差异。接下来我们将呈现 Yarn 和 Pnpm 两者的依赖管理差异:
Yarn Workspace:
1、在 package.json 中添加配置如下:
{
"workspaces": ["packages/*"]
}
2、子模块间依赖配置如下:
// modulea
{
"name": "@xx/modulea",
"version": "1.0.0",
"dependencies": {
"@xx/moduleb": "1.0.0",
"@xx/modulec": "1.0.0"
}
}
3、根目录执行 yarn install 命令进行依赖安装,会自动关联子模块之间的模块依赖。其依赖树结构如下:
├── node_modules
│ ├── @babel
│ ├── @changesets
│ └── @xx # 幽灵依赖,来自子模块
│ ├── modulea -> ../../packages/moduleA
│ ├── moduleb -> ../../packages/moduleB
│ └── modulec -> ../../packages/moduleC
├── package.json
├── packages
│ ├── moduleA
│ ├── moduleB
│ └── moduleC
└── yarn.lock
Pnpm Workspace:
1、根目录添加 pnpm-workspace.yaml 配置文件,内容如下:
packages:
- "packages/**"
2、子模块间依赖配置如下:
// modulea
{
"name": "@xx/modulea",
"version": "1.0.0",
"dependencies": {
"@xx/moduleb": "workspace:*",
"@xx/modulec": "workspace:*"
}
}
3、根目录执行 pnpm install 命令进行依赖安装,会自动关联子模块之间的模块依赖。其依赖树结构如下:
├── node_modules
│ ├── @babel
│ └── @changesets
├── package.json
├── packages
│ ├── moduleA
│ │ └── node_modules
│ │ └── @xx
│ │ ├── moduleb -> ../../../moduleB
│ │ └── modulec -> ../../../moduleC
│ ├── moduleB
│ │ └── node_modules
│ │ └── @xx
│ │ └── modulec -> ../../../moduleC
│ └── moduleC
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
由上可知,使用 Yarn 会将子模块的依赖项向最外层扁平化展开,这就会造成幽灵依赖现象,导致项目的依赖关系不够清晰,给开发者带来一定困惑。相比之下,Pnpm 的依赖树结构更符合常规认知,整个依赖关系更加透明和可控。因此,最终我们决定选择 Pnpm 作为 Monorepo 项目的依赖管理工具。
TurboRepo的优势
多任务并行处理
Turbo支持多个任务的并行运行,我们在对多个子包编译打包的过程中,turbo会同时进行多个任务的处理。在传统的 monorepo 任务运行器中,就像lerna
或者yarn
自己的内置workspaces run
命令一样,每个项目的script生命周期脚本都以拓扑方式运行(这是“依赖优先”顺序的数学术语)或单独并行运行。根据 monorepo 的依赖关系图,CPU 内核可能处于空闲状态——这样就会浪费宝贵的时间和资源。
什么是拓扑 ? 拓扑是一种排序 拓扑排序是依赖优先的术语, 如果 A 依赖于 B,B 依赖于 C,则拓扑顺序为 C、B、A。
比如一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的
为了可以了解turbo多么强大,下图比较了turbo vs lerna任务执行时间线:
![image-20241212232502329](/Users/guang/Library/Application Support/typora-user-images/image-20241212232502329.png)
Turbo它能够有效地安排任务类似于瀑布可以同时异步执行多个任务,而lerna一次只能执行一项任务,所以Turbo的性能不言而喻。
更快的增量构建
如果我们的项目过大,构建多个子包会造成时间和性能的浪费,turborepo中的缓存机制 可以帮助我们记住构建内容 并且跳过已经计算过的内容,优化打包效率。应该是借鉴了nx。
云缓存
Turbo通过其远程缓存功能可以帮助多人远程构建云缓存实现了更快的构建。
任务管道
用配置文件定义任务之间的关系,然后让Turborepo优化构建内容和时间。在 Turborepo 中有个 Pipelines 的概念,它是由 turbo.json
文件中的 pipeline
字段的配置描述,它会在执行 turbo run
命令的时候,根据对应的配置进行有序的执行和缓存输出的文件。
基于约定的配置
通过约定降低复杂性,只需几行JSON 即可配置整个项目依赖,执行脚本的顺序结构。
浏览器中的配置文件
生成构建配置文件并将其导入Chrome或Edge以了解哪些任务花费的时间最长。这点还比不上nx,nx可以直接生成拓扑图
Turbo 核心概念
包括pipeline
,DependsOn
,拓扑依赖,Output
,Caching
, Remote Caching
等,可以看官网有详细的文档描述。