背景
无论是开发应用还是二方库,monorepo 确实带来很多方便之处,之前一直使用的是 lerna + npm 作为 monorepo,由于公司无法使用 pnpm 也受限于 node版本的限制(部署阶段)
所以一直没有用上pnpm 和 turborepo/nx,但是最近想新开一个开源项目,想趁着这次机会做一次相对完整的调研,弥补当时的遗憾。
这篇文章是我了解到的 monorepo 相关的方案,如果你也需要对你的项目进行改造,我相信这篇文章应该能帮助对monorepo有一个初步的认识。
如果你在工作中已经使用monorepo,你也可以在评论区聊聊你使用monorepo有哪些痛点。
目标
我们先给自己制定一个目标
- 了解有哪些方案
- 每个方案有哪些功能
- 它们适用于什么样的场景
Demo
我们先来直观的看一下monorepo 长什么样
以上是我们创建的一个monorepo
由两部分组成
- 有一个根项目,他本身也是一个package
- 在根项目下 有多个子package,通常放在packages/apps目录下,当然也可以用其他的命名
monorepo与普通的package最大的区别就是,在一个package下包含其他的package
monorepo 项目具备两个优点
-
本地的相互依赖
在monorepo,如果你在packageA 中安装packageB,安装的事实上是本地的packageA,在packageA的node_module下packageB只是一个软链接
-
一键运行多个package的命令
在根项目中运行一个命令,会自动运行子项目中所有相关的命令,这样可以达到一键启动多个项目,避免手动启动多个项目,或者一键安装所有项目的依赖。
想要具备上述两个功能,我们必须要启用npm包管理的特性workspace,npm包管理发展至今,pnpm、yarn、npm 都具备workspace特性,那我们来看看这workspace 是什么?如何开启?
WORKSPACE
Workspaces 是一个通用术语,它指的是 npm cli 中的一组功能,提供了管理多个包的支持,这些包来自于您本地文件系统中的一个顶级根包。我们还将这些在 npm install 期间自动建立符号链接的包称为单个工作空间,这意味着它是当前本地文件系统中显式定义在 package.json workspaces 配置中的嵌套包。
上述是npm对workspace的定义
- workspace npm用于管理多个包的特性。
- workspace 需要在package配置,目的是为了告诉npm,哪些package 组成这个workspace
从上述我们可以看出来,workspace 是monorepo的基础。
npm workspace
配置
jsx
// package.json
{
"name": "my-workspaces-powered-project",
"workspaces": [
"packages/module-a",
"packages/module-b"
]
}
安装某个依赖
jsx
npm install lodash -w react-app
npm install module-a -w react-app
npm install
运行某个package的命令
jsx
// 在module 下运行test
npm run test --workspaces=module-a
// 运行所有pkg 的test
npm run test --workspaces
运行结果
这是运行test的结果,我们发现运行是线性的,虽然这几个package的test没有任何关系,很显然如果可以并行效率会更高。
看到这里你应该知道什么是 monorepo 了,而且怎么用 monorepo 应该有一定的认识了,除了npm之外其他包管理也有同样的功能,如果你好奇的话,不妨一起比较一下。
yarn workspace
配置与npm相同,但是命令有所不同
jsx
yarn workspaces run test
npm run test --workspaces
pnpm workspace
- 配置文件不同
pnpm-workspace.yaml
yaml
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'components/**'
# exclude packages that are inside test directories
- '!**/test/**'
-
协议不同
pnpm 支持协议 workspace: 这样更加直观,缺点就是你用npm 安装的话就会失败
yaml
{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0"
}
}
看到这里,你对workspace 应该有了比较直观的认识
我们可以总结一下,workspace 是包管理工具特性,方便monorepo 中依赖管理与命令的运行,
通过workspace 可以搭建一个简单的 monorepo,当然这只是入坑的第一步,如果真的只是启用workspace你会发现很多坑等着我们去踩。
版本管理
monorepo 修改一个package 与之相关的package的版本号也需要修改,我们想象一下:
有三个模块A、B、C
他们的关系是这样的
A依赖于B,B依赖于C ( A→B→C)
为了修复A中的bug,我们必须要对C做出改动,即便B不做改动,B中C的版本也需要升级,
如果我们手动维护A、B、C的版本号是一件非常累也很容易出错的事情。
因此我们需要引入新的工具来实现版本的自动管理 lerna/changeset,这两个工具有所不同
lerna 包含了workspace 的部分功能(安装依赖),同时也包含 版本的管理
changeset 不仅包含了版本的管理,同时也会生成修改日志,这对于一个好用的 npm package来说至关重要。pnpm 在官网就推荐使用 changeset
任务管理
随着monorepo 中package的增加,运行所有项目的build 可能时间会随之增加
假设我们本来只需要修改moduleA,moduleA 只依赖于 moduleB,与 C、D、E 毫不相干,但是我们为了在开发是调试我们有两种选择
- 构建moduleA和moduleB
- 构建所有的package
两者看上去都不是很好的方案,所以我们需要一种更聪明的运行命令的方式,那就是 nx 或 turborepo
他们是通过什么机制实现运行任务时间加速的呢
本地缓存
如果package运行过一次,并且这个package所依赖的package都没有发生过任何改动,那么基本可以断定这个package build 的结果也不会发生变化,直接采用上次 build 的结果这样就实现了执行任务的加速
共享缓存
nx 或 turborepo 都支持将构建的结果上传只云端,这样如果同事小强如果曾经构建过某个package,并且我没有任何改动,那么我就无需重新构建,nx/turborepo 会自动从云端下载缓存供我使用
任务编排
假设我们有一个如下图所示的monorepo
ssr-server-headless 不依赖于任何项目
- 所以 ssr-server-headless 的构建完全可以与其他任务同步进行
- news-app 的构建必须要求 norejs/ssr-cli 构建完成后才能构建
nx 和 turborepo 都可以通过编写任务之间的关系,确保任务有序、高效进行
除此之外必须介绍一下nx、turborepo 的更多特性,来感受一下他们能给我们带来什么
NX VS turborepo
NX
-
依赖关系图可视化
上图每个package之间的关系就是用nx生成的,在这一点上turborepo并不支持。
-
Affect
NX支持探测当前改动,只编译你的改动相关的 package, 例如你只改动了packageA,那么packageA 与 依赖了packageA的所有package 会运行构建,turborepo也支持这一点
turborepo
-
环境变量探测
编译结果有时候会根据当前机器的环境变量编译出不同的结果,比如NODE_ENV 我们通常就放在环境变量中,如果我们在本地的环境变量NODE_ENV= local,编译的结果被缓存起来,部署过程中如果直接采用了 我们本地编译的结果,那最终的结果是灾难性的。turborepo 提供了环境变量的探测,如果两次编译的环境变量不同,即便源代码相同也不会采用缓存,当然具体探测哪些环境变量需要手动配置
对比图
NX | turborepo | |
---|---|---|
本地缓存 | 支持 | 支持 |
远程缓存 | 支持 | 支持 |
自定义远程缓存服务器 | 不支持(开发中) | 支持(有开源项目) |
环境变量探测 | 不支持 | 支持 |
依赖可视化 | 支持 | 不支持 |
Affect | 支持 | 支持 |
到这里我们的monorepo 在 nx 和 turborepo 的加持之下变得更好用了,我们实现了运行的加速,我们实现了运行任务关系的绑定,避免执行多余的命令。
局限性
一切看上去距离完美更近了一步,但是仔细想一下它还是有一些缺点
脆弱的缓存
假设我们只是修改一个环境变量或者一个 package ,与之相关联的 package 缓存就会失效,缓存非常容易被丢弃。
可能误用缓存
由于是否采用缓存 nx 和 turborepo 都是采用源代码计算 hash 作为是否采用缓存的依据,
假设我们有个package,它依赖于根项目中的一个文件.env,如果.env 中的变量修改了,nx 会认为项目源代码并没有发生改变,从而缓存被复用,所以在使用是需要考虑到这一点,避免这种事情发生。
DX的提升有限
在开发阶段通常情况下需要 watch,并且有时候没有导出文件,所以 nx/turborepo 无法缓存结果,这种情况下对于开发体验的提升变得比较有限
小结
综上所述,nx 和 turborepo 对于加速 monorepo 的构建在使用得当的情况下,有事半功倍的效果,虽然某些特殊情况下容易出错,并且有些情况下缓存帮助不大,但是在任务编排方面的作用是毋庸置疑的。
nx和turborepo中的缓存粒度比较大,都是以package为维度进行缓存,如果想要更快的开发构建速度,可能需要更小粒度的缓存,turbopackage 以函数作为缓存粒度,这将会大大减少重复的构建,但是目前它还在开发中。
总结
- workspace 是 monorepo 的基础,便于安装本地的依赖,运行命令,npm,yarn,pnpm 都支持
- 为了便于发布和版本管理我们需要引入lerna/changesets ,但是更推荐使用 changesets
- 如果package比较多,可以利用nx/turborepo 来加速构建,避免构建不必要的package,但是使用时需要注意避免误用缓存,即便不用缓存特性,对于任务编排而言,这两者都是很好的工具。