引言
笔者最早接触 monorepo 的概念是在去年年中的时候,当时组内有需求写一个组件库,于是在设计之初有人提议使用 Lerna 来管理项目,便有了第一次与 monorepo 接触的契机,但后来发现没什么必要,便没有使用。在此之后,由于开源社区使用 monorepo 的项目越来越多,如 vue3.0 使用的 yarn workspaces、slidev 使用的 pnpm workspace笔者对这项技术的好奇心越来越高,并且客观事实证明 monorepo 对复杂项目的管理有很多的帮助,本文主要目的是为了建立一些基本认知。
如有错误,还请不吝斧正。
一致性
为了方便理解并防止分歧,有必要对本文内一些可能混淆的概念进行强调并释义。
请注意,释义仅对本文负责,并不一定适用于本文以外的内容,请注意甄别。
名词表
名词名称 | 释义 |
---|---|
项目 | project,抽象概念,在本文中可以将一个 package.json 对应的一个工程视为本文中的“项目”,或者也可以理解为一个单独的库。 |
根项目 | 负责统一管理项目的项目,公共配置一般在根项目配置 |
子项目 | 被管理的项目 |
存储库 | repository,由你的版本管理工具(git,SVN)所限定的一个工程整体。 |
包管理工具 | package manager,如 npm、yarn、pnpm、jspm 这类的依赖管理工具。 |
假设
为了方便理解,使得读者情感带入,我们需要先假设场景。
假设你现在是公司几个基础项目的维护者,其中有几个项目是这样的,
- 一个基于 webpack 封装的打包工具
- 一个基于该打包工具的快速生成项目的 CLI 工具
- 几个内部使用的 webpack 插件
你可能会相对频繁面临如下几个问题:
- 对三个项目的公共依赖需要进行统一的升级
- 其中一个项目更新,另外几个项目连锁反应也需要更新
那么该如何更好地解决呢?
monorepo 并不是银弹,但在适合的场景它可以解决不少的问题。
monorepo 介绍
What
monorepo (“mono” 的意思是 “单一”,“repo” 是 “repository” 的缩写)是一种软件开发策略,具体表现就是多个项目存储在一个存储库中。这种开发策略其实已经存在了许久,在前端流行之前便已经在存在许久,只不过在近几年才被明确定义。同时也属于版本管理控制的概念之一。
它与传统的一个项目一个存储库的策略背道而驰,其目的往往是为了统一管理项目的公共部分内容、拉近项目之间的依赖关系、缩短工作流。
Why
归功于前端技术的发展,前端应用的复杂度也随之上升,传统软件开发领域的策略也开始在前端流行,monorepo 便是其中之一。其目的归根结底是为了更好管理项目,提升效率,当然有利有弊,需要开发者谨慎选择。
什么场景会需要 monorepo?
Be water, my friend.
其实很难说清楚什么场景需要 monorepo,笔者更想把这个问题变成“什么场景会需要将多个项目存储在一个存储库里?”来解答:
- 项目有较强的关联性
- 项目中存在不少重复的内容
- 你想拉近项目之间的距离,而不是让他们之间的关系遥不可及。
基本上来说,当你在开发工作中,总是需要 来回切换项目来完成“一”件事 的时候可能就需要 monorepo 来解决一些问题。
monorepo 就一定需要专门的工具(库)才能实现吗?
答案当然是否定的,严格意义上说,只要将多个项目放在一个存储库里就算 monorepo。
但是现在主流的 monorepo 所承担的责任并不只是存储的问题,还可以承担比如依赖管理,增量构建等一系列工程化的功能,已经成为工程化技术中非常有价值的一块领域,所以有时你为了实现某个特殊的功能不得不借助社区的力量,或者站在大佬的肩膀上。
利弊
益处
- 公共事务统一处理:比如 eslint 配置、tsconfig 配置等。
- 及时的依赖关系:通常项目和项目之间需要通过 node_modules 来联系,但是在不少 monorepo 解决方案中,你可以直接本地及时响应有依赖的变动。
弊端
- 项目结构复杂:如果不注意保持项目结构整洁,很容易会导致项目结构十分复杂。
- 学习成本较高:不管是基于很重复杂程度的解决方案都会带来一定的心智成本,耦合程度越高,心智成本越高。
社区方案
不同的场景,有着不同的需求,所以下面介绍的方案并不一定适用所有场景。
Workspaces
首先介绍的是主流包管理工具的 workspaces 特性,借用 Workspaces in Yarn 里对 workspaces 的介绍:“workspaces 是一种可以将多个由 package.json
为单位的项目统一在一个也以 package.json
为单位的项目里面,他们的依赖可以被统一管理。”
包管理工具利用其对于依赖管理得天独厚的优势,可以直接无痛的对项目依赖作出干涉,从而实现了依赖层面的统一管理。
值得注意的是,包管理工具的 workspaces 特性已经成为其他更上层的 monorepo 工具的基础设施之一。
所以 workspaces 不仅是 monorepo 的轻量解决方案之一,同时也是目前其他解决方案的基础,也是至关重要的环节之一,workspaces 自身的质量决定了上层建筑的好使程度。
接下来,简单罗列一下各个包管理工具的 workspaces 特性和注意点(主观观点:从上至下,由差到好)。
Demo 项目地址:https://github.com/yingpengsha/workspace-research (里面有更详细的笔记哦)
包管理工具 | 准备工作 | 依赖统一管理 | 添加本地子项目为依赖 | 注意点 |
---|---|---|---|---|
yarn2 | 配置 workspaces 字段 |
✅ 支持 | ⚠️ 支持,但使用的是 yarn2 的 workspace 协议 | - 功能很多,基本可以和大型的 monorepo 解决方案媲美。 - 但 yarn2 自身可用性和稳定性尚待考验,请谨慎选择。 |
yarn | 配置 workspaces 字段 |
⚠️ 默认不支持(需要关闭 nohoist ) |
⚠️ 支持,但第一次添加依赖需要指定版本号 | - 不支持子项目为依赖 - yarn 的 bug 太多了,官方不怎么维护。 - 如果没有太多需求需要实现,可以一试。 |
npm | 配置 workspaces 字段 |
⚠️ 支持(子项目需要通过专门的指令) | ✅ 支持 | - 起步较晚,需要很新版本的 npm - 但是功能稳定且实用,值得一试 |
pnpm | 添加 pnpm-workspace.yaml 文件 |
✅ 支持 | ⚠️ 支持,但使用的是 pnpm 的 workspace 协议 | - 好使 👍 |
小结
将 workspaces 作为 monorepo 的解决方案在大部分的场景其实已经够用,并且心智成本也不高,只需要统一包管理工具即可,所以笔者极力推荐读者先尝试只用workspaces 做为解决方案,如果不行也可以试试自己用脚本拓展一下,再不行再使用其他工具。
接下来介绍的都是一些成本稍高,但覆盖链路更长的社区解决方案。
Lerna
大名鼎鼎的 Lerna,成熟且被广泛使用的 monorepo 解决方案,并且可以结合 yarn workspaces 来进行管理,如此一来,它不仅支持依赖的共享和本地的互相依赖,它还支持更智能的版本控制及更方便的发布操作。
抛去和 workspaces 重合的依赖管理部分,我们简单过一下它其他值得关注的功能:
更方便的项目管理
$ lerna import <pathToRepo> --dist=<targetPackageName> # 将已经存在的项目添加到子项目中
$ lerna create <packageName> # 快速添加子项目
通过 git 检查项目变动
$ lerna changed # 检查自上次发布以来有哪些包有更新
$ lerna diff # 查看自上次发布以来的所有包或者指定包的 git diff 变化
可以按照拓扑顺序执行 build 指令
$ lerna run --stream --sort build
更加智能的发布脚本
$ lerna publish # 发布自上次发布以来有更新的包,包含了lerna version的工作。
$ lerna publish form-git # 显示发布当前提交中标记的包,类似于先独立执行lerna version后,再执行此命令进行发布。
$ lerna publish from-package # 显示发布npm registry中不存在的最新版本的包。
小结
Lerna 在各种项目验证过后,证明了它是一个可以扛得起长链路和复杂项目 monorepo 解决方案,如果你的项目需要较长的工具链去完成更多的工作则可以试试 Lerna。
但由于 Lerna 往往与 yarn workspaces 一起使用的问题,yarn 的一些毛病可能会传染到 Lerna 身上:
- 首先与 yarn workspaces 结合会导致命令太多,如
yarn <command>
、yarn workspace <command>
、lerna <command>
。 - 幻影依赖,这实际上是因为 yarn 或者 npm 说自身导致的,因为其将依赖在
node_modules
铺平的策略实际上与 nodejs 自身模块查找策略之间产生了一个灰色地带,从而导致你可以在代码中使用在package.json
并没有显式的安装依赖,从而导致不一致的错误。 - yarn.lock 冲突问题
- …yarn 的众多 bug
Rush
Rush 是由微软开发的 monorepo 解决方案,它可以结合 pnpm 来使用,所以解决了很多 yarn 和 npm 的问题,同时具备着更强大的链路设施。
丰富的管理策略
统一的指令
rush 一把梭即可
增量构建
$ rush build # 按照变动情况增量构建,当然会将其依赖的变动也重新构建一边
$ rush rebuild # 完整干净的重新构建一遍
开放接口,自定义实现更复杂的功能
小结
Rush 的功能很多,自定义能力也很强,笔者没有太过深入的了解全部功能,但毫无以为是,对于超大型的 monorepo 项目 rush 因为其丰富的功能和 API 完全可以胜任。
总结
本文主要介绍笔者对于 monorepo 的思考,各个包管理工具的 Workspaces 功能以及 Lerna 和 Rush。
Workspaces 其实属于前端的 monorepo 解决方案中不可取代的一环,所以你想使用 monorepo 你需要好好的去研究一下各个包管理工具和其 workspaces 的优缺点。
在前文 monorepo 的场景笔者之前只考虑了什么样的项目需要 monorepo 这种策略,但实际上还有一种角度是直接一个公司单位的代码都用 monorepo 来管理,典型的应该就是 Google,所以 monorepo 的概念和一些设定其实在不同场景下的定义已经没这么具体了。
如果你还想继续深入的研究,推荐阅读以下本文的参考文献。
Be water, my friend.
参考
- Monorepo - Wikipedia
- korfuri / awesome-monorepo
- All in one:项目级 monorepo 策略最佳实践
- workspaces | npm Docs
- Workspaces | Yarn
- Workspaces | pnpm
- nohoist in Workspaces
- Getting Started with npm Workspaces
- lerna/lerna: A tool for managing JavaScript projects with multiple packages.
- Phantom dependencies
- 为什么我们应该使用 pnpm(译)
- lerna管理前端packages的最佳实践
- 精读《Monorepo 的优势》
- Lerna | 大笑文档
- Monorepo 的这些坑,我们帮你踩过了!
- 【译】配置 Monorepo 的几种工具 lerna、npm、yarn 及其性能对比
- Why Google Stores Billions of Lines of Code in a Single Repository
- Advantages and Disadvantages of a Monolithic Repository