Monorepo 即代码的仓库维护方式,已经被诸多知名互联网公司如 Google、Meta、Microsoft 等公司使用了很多年,该模式的主要特点是将所有代码都集中到一个仓库中管理。与 Monorepo 相对的是 Multirepo(又叫 Polyrepo),该模式下每个模块都有自己独立的仓库。Monorepo 是目前前端开源项目的趋势之一,Vue3、Yarn2 等知名项目均已改用 Monorepo 模式。
但对于业务型项目的 Monorepo,目前普及地还不是很广泛,虽然如此,业务型 Monorepo 也能给业务开发带来极大的便利。本文主要介绍业务型 Monorepo,使用它的好处,以及业务型 Monorepo 的工程化建设。
前端流行仓库管理模式 Monorepo 浅析
1. 简介
1.1 什么是 Monorepo
Monorepo 是一种代码管理模式,将多个项目组织到一个 Repo 中。它有以下一些优点:
- 子项目之间是独立的,可以独立开发、测试、以及部署
- 子项目的类型可以是任意的,可以是 web 项目、server 项目、或者 library 项目等等
- 子项目之间可能有依赖关系,例如一些顶层的项目会依赖于底层项目暴露的API
对于前端项目而言,Monorepo 的子项目可以大致分为两类:
- 业务类子项目:子项目需要部署上线,例如基于 React 的业务项目,特点是不需要发布到 npm registry
- lib 类子项目:前端的 package 或库,特点是需要 publish 到 npm registry
本文主要介绍业务型 Monorepo,即将多个业务应用(例如 React、Node等)以及其依赖(组件库、公共工具库等)集中到一个仓库中进行管理的模式,示例图如下:
目前全球很多知名互联网公司都在使用 Monorepo 管理其源代码,据悉,谷歌甚至将整个公司的代码都放到一个仓库中,其仓库大小高达80TB。
1.2 Why Monorepo?
随着业务的增长,一个团队往往会同时维护和迭代多个项目,并且这些项目之间往往需要共享代码(组件库、工具函数等等)、共享基础设施(构建工具、lint等)。然而,在 Multirepo 仓库管理模式中,跨项目的代码共享非常麻烦,常用的方式是将公共代码提取出来并放入一个单独的仓库,然后将公用仓库的产物发布到 npm registry(一般复用组件库就是采用这种形式),示例图如下:
在上图中,每一个顶层的 App 都在一个单独的仓库中,都有自己的一套基础设施、CI/CD 流程等,这些 App 的共用部分被拆分到独立的仓库中作为共用库,并发布到 npm,然后作为 npm 依赖引入到各个独立的 App 仓库中。看起来这是一个不错的解决方案,但是这种方法也会有一些问题:
重复的基础设施:每个项目都需要单独设置 CI 流程、配置开发环境、配置部署发布流程。这样不仅可能会带来一些构建设施或规范的不一致性,而且将会大大提高项目的基建维护成本。每个项目都需要专人维护,并且随着项目越来越大,维护成本会越来越高。
割裂的工作流:多个项目组成的工程体系是不连续的,每个仓库都有自己的一套工作流程。当进行多项目协作开发时,整体的工作流是割裂的,例如需要修改公共库的一个函数,首先需要修改和调试公共库的仓库,然后跑工具库的 CI 流程,然后升级需要使用该函数的 App 仓库的相应依赖,然后调试 App,这一套流程显得非常的繁琐且不连续。
复杂的版本管理:仓库之间的依赖关系随着时间推移会变得非常复杂。
采用 Monorepo 的模式管理代码可以很好地解决以上问题:
- 统一的 CI 流程和基建:一套工具、规范落地所有项目,无需重复切换开发环境,减轻协作开发的成本。只需要为 Monorepo 配置好基础设施,后续所有新建的项目都可以复用已有的基建,只需要1-2个同学维护所有项目的基建即可,降低了基建的维护成本。
- 一致的工作流:当工具库升级后,顶层的应用能同时感知到其依赖的升级,能够非常方便地调试工具库的修改,并能在开发完成后自动触发相关项目的测试发布流程。
- 简化的依赖管理:所有项目都使用 HEAD 上的代码,项目之间的依赖关系直接且清晰。
- 代码共享和团队协作能力:在 Monorepo 中,可以非常方便地实现代码复用,同时更加方便地检索到各项目的源代码,降低团队成员间的沟通成本。
简而言之,Monorepo 可以将多个独立的项目变成一个统一的工程,多项目具有高度一致性并且有更高的复用和抽象能力。
2. Monorepo 工具调研
简单来说,上层工具依赖下层工具提供的能力:
- 最底层—Package Manager:例如 npm, yarn, pnpm 等,提供 monorepo 下的依赖管理和依赖安装能力
- 中间层—Script Runner:例如 lerna, wsrun,提供在 Monorepo 中以某种顺序调用子项目的 script 或者在子项目下执行某种命令的能力
- 最上层—Application:例如 nx, rush stack,集成了 react, node server 等脚手架,为 monorepo 子项目的开发、构建、以及测试等工程能力提供支持
2.1 Package Manager
在各个 Package Manager 中,Monorepo 中的子项目称为一个 workspace,多个 workspace 构成 workspaces
支持 workspaces 特性的 package manager,一般用于 Monorepo 中的依赖安装。目前前端最常用的 Package Manager 有 npm、yarn、pnpm,对 workspaces 特性都有一定的支持:
- npm:npm 从 v7 开始支持 workspaces,从 node 15 开始内置 npm 7
- yarn:已稳定支持 workspaces
- pnpm:已稳定支持 workspaces
在 workspaces 模式下,package manager 会分析整个 Monorepo 下的所有 workspace,对所有 workspace 统一安装外部依赖,并且 link 依赖的 workspace 到node_modules 下。Package Manager 的 workspace 特性适合作为 Monorepo 的依赖管理方案,但是依赖管理是非常底层的能力。对于业务型的 Monorepo 来说,子项目的初始化、构建、以及部署都需要一整套工具链的支持。
2.2 Lerna
Lerna 是目前使用最为广泛的 Monorepo 工具之一,提供了依赖安装、自动升级多个项目的版本好并发布、批量在多个项目中按序执行特定命令的能力,lerna 的常用命令如下图:
Lerna 的能力是 Package Manager 和 Script Runner 的整合,在面向库的 Monorepo 中使用非常广泛,被开源项目和企业内部的工具库项目所广泛采用。但是对于面向业务的 Monorepo,Lerna 缺乏构建、测试等相应工具链的支撑。而且当 Monorepo 持续增大之后,Lerna 不支持增量构建、测试,缺乏 scale 能力。
2.3 集成的 Monorepo 工具
集成的 Monorepo 工具基于上述的 Package Manager,在其基础上集成了 Script Runner、构建工具、测试工具等,常用的工具有 rushstack、nx 等。rushstack 是基于 rushjs 的一套 Monorepo 解决方案,相较于 Lerna 等工具,其提供了 webpack、jest 等常用工具的封装,并提供了增量构建的能力。nx 的建设相较于 rush 更加的顶层,提供了 React、React Native、Next 等技术展的工程化支持。
rushstack 和 nx 这类集成的 Monorepo 工具,相较于底层的工具提供了较为全面的工程化能力,更加适合业务型 Monorepo 的开发。
3. 小结
对于较底层的工具,如 yarn、pnpm、lerna 等,这些工具仅仅用于依赖安装,或者按序调用 script,难以支撑起整个 Monorepo 的研发,缺乏整体工程化以及 scale 的能力。对于较顶层的工具,如 rushstack、nx 等,提供了较全的工程化能力,有一套完整的 Monorepo 工程设施,这类工具比较适合业务型 Monorepo 的研发,不过集成工具一般都有自己的生态体系和技术栈,因此有可能与现有的基础设施产生冲突。
参考
《Eden Monorepo 系列:浅析 Eden Monorepo 工程化建设》