5W1H 带你入门 Monorepo

avatar
FE @字节跳动

概述

无论是为了更高效的团队协作,还是改进代码管理,Monorepo 都是一种有效的方案。本文用「5W1H」带你了解以下内容:

  • 「What」什么是 Monorepo ?

  • 「Why」为什么会有 Monorepo ? 它的优势是什么 ?

  • 「Who」谁正在用 Monorepo ?

  • 「When」什么时候用 Monorepo ?

  • 「Where」去哪学习 Monorepo ?

  • 「How」如何从零构建 Monorepo?

正文

「What」什么是 Monorepo ?

随着大前端崛起,初始的 Polyrepo 方案承载不了日益复杂的业务场景。那什么是 Polyrepo?

什么是 Polyrepo?

Polyrepo 也称 multiple repostories。指多 Git 仓库结构,这与 Monolith 完全相反,只要能独立运行命令项目就切分成单独 git 仓库,井水不犯河水。在 Monorepo 概念浮现在大众之前,Polyrepo 是唯一的选择。

还有一个 Polyrepo 的进阶版:新建一个文件夹,将所有 Git 仓库都套进去,治标不治本。

那什么是 Monorepo ?

Monorepo 全称 Monolithic Repository,指单个仓库可包含许多相关但独立的项目,可以由同一个团队或多个团队管理。如下图可清晰看出 Polyrepo 与 Monorepo 区别。

image.png

我对 Monorepo 的浅薄理解:

  • 子项目间可独立运行、共享代码,但没有强依赖,剥离出来照样能运行,只是引入方式从 workspace 转变成 Npm,遵循高内聚,低耦合理念
  • 开发环境下应尽量少依赖或不依赖全局变量以及根路径下 node_module,如若违背该点,则第一点不成立

「Why」为什么会有 Monorepo ? 它的优势是什么?

每件新事物的出现必然是为了补充旧事物的不足。当我们用 Polyrepo 架构不断迭代时,达到一定数据量级时会出现以下问题:

  1. 大型项目被割裂成多个 Git 仓库:

    • 需要 git clone 多个仓库,并运行不同的命令来共同跑起完整的服务(典型:前端+后端+ SDK 项目分离)。
  2. 代码复用率严重下降:

    • 公共函数或组件只能通过 duplicate 或 npm 方式共享。
    • 当某个公用组件(对应 npm 包)修改时,需要人为确认该 NPM 包所依赖的各个项目升级情况
  3. 相同类型项目却有着千奇百怪的开发工具和周边库:

    • jest、java 的版本不一致,导致 API 写法不一
    • React 组件库以及状态管理库不一致,新人接手学习成本提高
    • ESlint 、CommitLint、prettier 配置 和 Changelogs 生成规则不一致
  4. 新建项目时有额外心智负担:

    • 如果有需要,可能要复制多套 CI 文件

    • 出于好奇,可能会找更符合当下的 cli 来新建项目(耗时且可能有坑)

    • 完美主义的人可能会配置更多的 eslint、prettier(耗时)

    • 需要在其他项目中 Readme 中添加新项目的 git 地址,在表面上报保持 Connection

那么 Monorepo 是如何一一解决上面的问题呢?

Monorepo 的优势

我们开发新项目时,必经的几个步骤依次是:「安装依赖」->「本地开发与调试」->「CI构建」->「发布与上线」

那么就从这四个视角来看看相对于 Polyrepo, Monorepo 有哪些优势。

安装依赖

ActionPolyrepoMonorepoMonorepo 收益 & 缺点
install 依赖次数项目个数即次数一次- 减少 install 命令次数

本地开发与调试

ActionPolyrepoMonorepoMonorepo 收益 & 缺点
服务关联启动在不同项目下的终端运行启动命令在当前项目下启用多个终端会话一键启动多项目服务
统一工具选型撰写新人入门文档,并在文档中说明工具选型方案
开发脚手架 cli,预设工具依赖
提取工具和其配置至独立包(推荐)或 root
减少团队其他成员接手成本
收敛三方包版本人为将所有项目 package.json 的版本统一👆🏻 做完上一步后直接带来的收益避免第三方包被打包多次
复用代码通过 NPM 方式引入进行复用
Copy 源码后粘贴至新项目
通过 workspace 在本地建立联系
通过 tsconfig 配置 paths 方式在开发时获得代码提示,打包时通过 alias 插件来解析路径
代码复用率提高,降低开发无用功
贡献代码询问同事某公用组件 => 安装公用组件 NPM 包 => 代码功能稍微不满足业务场景 => Fork仓库 => 功能完善 =>提交 PR => 发布新版公用组件 NPM全局搜索/询问同事某公用组件 => 通过 workspace 引入新组件 => 代码功能稍微不满足业务场景 => 功能完善 => 提交 PR前置提高寻找公用组件速度
引用、PR、更新公用组件便捷
代码规范统一将 eslint、tslint、prettier抽成 NPM 包,并督促所有项目负责人接入使用将 eslint、tslint、prettier 配置抽离成一个子包,并强制其余每个项目通过 workspace 来引入代码规范统一效率更快
代码规范规则更新无成本
可强制规范所有子项目
热更新借助 npm link 或 yalc 等工具更快捷地在本地调试 NPM 包通过 watch dist(产物) 或原始文件(ts 可通过 rollup、webpack 的 alias 来隐射地址)的变更追踪热更开发时更新文件即热更新
单元测试可查看单个项目单测覆盖
单测工具、配置多种多样
复用三方单测工具、配置,同场景单测配置一致便捷查看整体单测覆盖率
可便捷查看你正在引用或感兴趣项目的单测覆盖率以及单测逻辑
代码权限以 Git 仓库为单位,易做权限隔离借助三方工具,可做到指定人员只可更改指定子项目子项目权限颗粒度较难管控
Git单个项目不会很大,git 的所有命令速度都很快当整体项目达到几百 G 甚至几 T 时,git 命令稍微会有点卡顿Git 命令运行性能下降

CI 构建

ActionPolyrepoMonorepoMonorepo 收益 & 缺点
lint/test/build各自构建,宏观意义的并行处理
没有改动就没有发版,天然优势的构建缓存
增量构建:根据 package.json 下的依赖可产出有向无环图,通过拓扑排序并行构建此次改动涉及到的项目
Nx: affected
Rush: Incremental builds
增量构建
微观意义的并行构建

发布与上线

ActionPolyrepoMonorepoMonorepo 收益 & 缺点
版本管理人为维护版本各个子项目版本根据依赖有向无环图和改动文件,可按照 Semantic 协议来 Bump 所有涉及到项目的 Version
Rush version
Lerna version
changeset version
避免人为维护导致的版本依赖异常
发布单次 CI 单次发布单项目
人为 check 发包顺序
人为 check 发布完成后的版本依赖
根据依赖有向无环图和已更新完后的 version,可一次按照拓扑排序批量发布
Rush publish
changeset publish
避免人为 check 导致发包顺序、版本依赖异常
提升批量发布的速度

「Who」谁正在用 Monorepo ?

以上一一列举了 Monorepo 的优势,那么现阶段都有哪些明星项目使用它呢?据不完全统计,现 Github 上有很多项目都是类 Monorepo:

以上各个项目的实现各不相同,比如 Vue3ReactRxjs 项目建立以来便用脚本实现基本的多项目打包、发包、以及版本检测。监控界老大哥 Sentry 用的是 Lerna 3.x,再如近期较火的 Vite 用的是 Pnpm + workspace。虽技术栈各不相同,但其目的都是为了更好的管理多个子项目。

「When」什么时候用 Monorepo ?

当你意识你的仓库现存在打包、发版缺陷或需要某些硬编码脚本时,可能你就需要 Monorepo 来工程化管理所有子项目。

我们假设有一个场景:你正在维护三个包,package_3 -> package_2 -> package_1

image.png

Polyrepo 实现

  • 每个包都有 rollup、jest、tsconfig 的配置文件,内容几乎一致

  • package_1 是最核心包,没有被任何依赖,可以随心所欲的 publish

  • package_2 依赖 package_1,当 package_1 更新时,需手动更新 dependencies 后 publish

  • package_3 依赖 package_2,当 package_2 更新时,需手动更新 dependencies 后 publish

显而易见,以上 Polyrepo 的实现在版本更新迭代时需要人为更新 dependencies,毫无工程化可言,那么转成稍微接近 Monorepo

Near Monorepo (较为常用)

  • rollup、jest、tsconfig 的配置文件都放置 root 文件夹下,rollup 的 entry 变成了多个
  • packages 下的包依赖从 npm依赖 => workspace 依赖,当 package_1 更新时,package_2、package_3 自动升级版本(changeset version

现市面上的部分明星项目就是以上这个模式,比如:vue3,因为是纯 JS 库,开发配置相对简单,不需要对 root 下的依赖进行抽离和管理。但如果现在新增一个 package_4,里面包括 react、vue 组件,最外层的配置就会愈来愈多。配置变更如下图所示:

Authentic Monorepo(rushjs 推荐)

为了解决上面根路径下的配置文件愈来愈多的情况,那我们能不能把所有配置文件都抽成独立包来维护?答案是可以。如下图所示,将公用配置都抽成特定包放入 utils 文件夹下维护。

有的人就说了,只是将这些配置文件存放挪了一个位置而已,有啥用呢?那么列举两个这样做的好处:

  • 增强可迭代性:随着项目个数和复杂度的增加,root 文件夹下配置文件并不会臃肿

    • 假设新进来一个 rust 项目,它根本不需要 root 下的 eslint、tsconfig 配置,放着还有点碍眼
  • 更符合 Monorepo 的概念:每个子包之间不应以非正常关系关联

    • 子包间通过相对路径来引用

「Where」去哪学习 Monorepo ?

「How」如何从零构建 Monorepo?

从零开始,自己动手,丰衣足食

想从零开始搭 Monorepo 的小伙伴,推荐用 pnpm + workspace 搭建如上图所示的乞丐版 Monorepo demo,不需要其他额外的工具,Pnpm 提供 filter 命令,基本可满足大部分场景。可以试着通过 Pnpm 现有的命令去搭建如下面两张图所示的乞丐版 Monorepo。这里推荐 Pnpm 的官方仓库,它本身是一个非常好借鉴的例子,还有谁能比 pnpm 开发者更懂基于 pnpm 的 Monorepo 呢 😀

基于 pnpm + workspace,我已搭建了上述的 demo 可供参考,项目地址

借助工具,快速构建

当你的项目需要根据 package.json 下的依赖进行拓扑排序并行打包、增量打包、打包缓存、依赖图可视化等等各种高阶需求,此时可以找一些开源三方工具来助你更高效的完成这些事。以下表格对比属个人看法。

工具发展史使用层面优点
lerna && NxLerna 在 2021 年更新至 4.0.0 时,一度放弃维护,甚至在 issue 表明不会再次迭代。后续被 Nrwl 团队 接手后整合了 NX 的一些功能,lerna 现在已更新至 6.2.0。刚好 Nx 本身不提供类似多包的版本管理,整合了 lerna 后刚好填补一块小空缺。推荐先用 pnpm + workspace 来进行搭建 Monorepo,随后遇到解决不了的问题或需耗时编写脚本才能解决的问题时,带着问题去 Nx 文档寻找解决方案 。由于 Nx 不会对项目架构做任何的改动,只需要在 root 文件夹下添加一个配置文件即可。接入配置简单
Polyrepo 迁移较为方便
现成功能较为齐全:依赖拓扑图增量运行
Turborepo一开始用 Go 语言实现了包之间的拓扑并行打包、增量打包、打包缓存(远端缓存)。后面又推出 Turbopack alpha:底层基于 swc 开发,用于 js 和 ts 的增量打包器。两者现合并成一个仓库:turbo。且 Turbopack alpha 的推出,一下让不到 10K star Turborepo 立即涨到了 18K。接入方式与 Nx 较为相似,只需在 root 文件夹下添加一配置文件来声明任务编排的顺序即可。单论现有能力和稳定性的话 Nx 是完全大于 Turborepo,毕竟迭代时间差距较大( rxjs 也在迁移至 turborepo,但是迁到一半时不知为啥,无动于衷 😂)接入配置简单
背靠明星团队 vercel,后续可能与 Turbpack 耦合发挥更大潜力
Rushstack && rushjs--相对前两者:
它更趋近于 Monorepo,倡导纯净根目录:保持这种严格规范下,所有子包和 root 完全解耦。
全局命令约束力强:全局命令都配置在 common/config/command-line.json,有效避免命令泛滥
提供架构思想、约束力强。这也提高了入门门槛。
如果从零开始搭建,建议试试 Rushstack
现成功能较为齐全:
-批量&增量运行
Monorepo 周边齐全:
- dts 提取和文档生成工具:API Extractor & API Documenter
-预设 Eslint:@rushstack/eslint-config...etc
API 接口暴露较多,可自定义能力较强

总结

Monorepo 与 Polyrepo 各有千秋,最终的选择还是要根据实际情况而定。最后有请 ChatGPT 来总结下两者的优劣势 🥳。