概述
无论是为了更高效的团队协作,还是改进代码管理,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 区别。
我对 Monorepo 的浅薄理解:
- 子项目间可独立运行、共享代码,但没有强依赖,剥离出来照样能运行,只是引入方式从 workspace 转变成 Npm,遵循高内聚,低耦合理念
- 开发环境下应尽量少依赖或不依赖全局变量以及根路径下 node_module,如若违背该点,则第一点不成立
「Why」为什么会有 Monorepo ? 它的优势是什么?
每件新事物的出现必然是为了补充旧事物的不足。当我们用 Polyrepo 架构不断迭代时,达到一定数据量级时会出现以下问题:
-
大型项目被割裂成多个 Git 仓库:
- 需要 git clone 多个仓库,并运行不同的命令来共同跑起完整的服务(典型:前端+后端+ SDK 项目分离)。
-
代码复用率严重下降:
- 公共函数或组件只能通过 duplicate 或 npm 方式共享。
- 当某个公用组件(对应 npm 包)修改时,需要人为确认该 NPM 包所依赖的各个项目升级情况
-
相同类型项目却有着千奇百怪的开发工具和周边库:
- jest、java 的版本不一致,导致 API 写法不一
- React 组件库以及状态管理库不一致,新人接手学习成本提高
- ESlint 、CommitLint、prettier 配置 和 Changelogs 生成规则不一致
-
新建项目时有额外心智负担:
-
如果有需要,可能要复制多套 CI 文件
-
出于好奇,可能会找更符合当下的 cli 来新建项目(耗时且可能有坑)
-
完美主义的人可能会配置更多的 eslint、prettier(耗时)
-
需要在其他项目中 Readme 中添加新项目的 git 地址,在表面上报保持 Connection
-
那么 Monorepo 是如何一一解决上面的问题呢?
Monorepo 的优势
我们开发新项目时,必经的几个步骤依次是:「安装依赖」->「本地开发与调试」->「CI构建」->「发布与上线」
那么就从这四个视角来看看相对于 Polyrepo, Monorepo 有哪些优势。
安装依赖
| Action | Polyrepo | Monorepo | Monorepo 收益 & 缺点 |
|---|---|---|---|
| install 依赖次数 | 项目个数即次数 | 一次 | - 减少 install 命令次数 |
本地开发与调试
| Action | Polyrepo | Monorepo | Monorepo 收益 & 缺点 |
|---|---|---|---|
| 服务关联启动 | 在不同项目下的终端运行启动命令 | 在当前项目下启用多个终端会话 | 一键启动多项目服务 |
| 统一工具选型 | 撰写新人入门文档,并在文档中说明工具选型方案 开发脚手架 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 构建
| Action | Polyrepo | Monorepo | Monorepo 收益 & 缺点 |
|---|---|---|---|
| lint/test/build | 各自构建,宏观意义的并行处理 没有改动就没有发版,天然优势的构建缓存 | 增量构建:根据 package.json 下的依赖可产出有向无环图,通过拓扑排序并行构建此次改动涉及到的项目 Nx: affected Rush: Incremental builds | 增量构建 微观意义的并行构建 |
发布与上线
| Action | Polyrepo | Monorepo | Monorepo 收益 & 缺点 |
|---|---|---|---|
| 版本管理 | 人为维护版本各个子项目版本 | 根据依赖有向无环图和改动文件,可按照 Semantic 协议来 Bump 所有涉及到项目的 Version Rush version Lerna version changeset version | 避免人为维护导致的版本依赖异常 |
| 发布 | 单次 CI 单次发布单项目 人为 check 发包顺序 人为 check 发布完成后的版本依赖 | 根据依赖有向无环图和已更新完后的 version,可一次按照拓扑排序批量发布 Rush publish changeset publish | 避免人为 check 导致发包顺序、版本依赖异常 提升批量发布的速度 |
「Who」谁正在用 Monorepo ?
以上一一列举了 Monorepo 的优势,那么现阶段都有哪些明星项目使用它呢?据不完全统计,现 Github 上有很多项目都是类 Monorepo:
以上各个项目的实现各不相同,比如 Vue3、React、Rxjs 项目建立以来便用脚本实现基本的多项目打包、发包、以及版本检测。监控界老大哥 Sentry 用的是 Lerna 3.x,再如近期较火的 Vite 用的是 Pnpm + workspace。虽技术栈各不相同,但其目的都是为了更好的管理多个子项目。
「When」什么时候用 Monorepo ?
当你意识你的仓库现存在打包、发版缺陷或需要某些硬编码脚本时,可能你就需要 Monorepo 来工程化管理所有子项目。
我们假设有一个场景:你正在维护三个包,package_3 -> package_2 -> package_1
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 && Nx | Lerna 在 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 来总结下两者的优劣势 🥳。