背景
Monorepo 就是把多个包放到一个仓库中,通过统一的流程来管理。这些包可能相互独立,也可能相互关联,或者有依赖关系。
Google、Facebook、Twitter 这些公司都会把很多包放到一个仓库中管理,所以会有巨大的 Monorepo。仓库太大时会带来很多别的问题,比如网络问题、硬件限制、git操作会变慢,所以很多开源项目仅用 Monorepo 来管理一个项目相关的包,比如核心功能和官方组件,比如著名的 Babel。
为什么要使用 Monorepo
-
让每个包只做一件事情,而且把它做好。
项目大了之后难以维护,所以进行拆分是必然的结果,借助包管理器可以更好地复用代码,避免出现一个问题要在不同的地方都修复一遍。
-
支持包之间的依赖关系,无需发布。
包之间存在依赖关系时,不需要发布内测版进行调试,而是直接在 Monorepo 中建立依赖的 link 即可。
-
整合开发流程,批量操作。
通过统一的规范,可以批量地对仓库中所有的包进行统一管理,统一编译和发布,及时更新依赖版本,避免出现漏更新、版本不匹配的情况。
此外,可以建立统一的工具链,减少拆分时每个项目的模板代码。
-
方便重构代码。
因为所有相关的代码都在一个仓库中,所以修改 API 变得非常方便,在 TypeScript 的帮助下更加可以确保接口变更时所有的类型都是匹配的,不论是人工操作还是代码感知的方式在 Monorepo 中都会更加方便。
-
共享依赖。
NPM 依赖有个严重的问题就是磁盘资源占用巨大,而 Monorepo 中的包通常会有很多相同的依赖,通过一些工具的辅助,可以将整个 Monorepo 中相同的依赖共享,从而大大地节约磁盘空间。
如何创建 Monorepo
Babel 开发者提供了一个强大的工具用于管理 Monorepo:lerna。
lerna 是一个管理 monorepo 的工具,它提供了很多方便的命令,用于批量操作仓库中的包。
通过 lerna init
就可以创建一个 Monorepo,它的目录结构如下:
.
├── lerna.json
├── package.json
└── packages/
├── package-1
├── package-2
└── package-3
packages
下面每个目录对应 Monorepo 中的一个包。
lerna
提供了一系列的命令可以操作这些包,比如:
lerna bootstrap
可以初始化所有包的依赖,包括同一个仓库中不同包之间的依赖关系。lerna publish
可以批量发布所有发生变化的包。lerna exec -- npm run build
可以在所有包中执行npm run build
。
默认情况下, lerna
初始化后会在每个包下面都安装自己的依赖,生成对应的 node_modules
,所以并不能起到依赖共享的作用,还是会占用很多空间。举个例子:
.
└── packages/
├── package-1/
│ └── node_modules/
│ ├── common1
│ └── a
├── package-2/
│ └── node_modules/
│ ├── common1
│ ├── common2
│ └── b
└── package-3/
└── node_modules/
├── common2
└── c
这时如果我们使用的是 npm
包管理工具,可以使用 lerna bootstrap --hoist
来初始化依赖,它会自动分析所有包的依赖,将相同的部分提取出来,放到仓库根目录下的 node_modules
中,从而避免重复安装。得到的结构如下:
.
├── node_modules/
│ ├── common1
│ └── common2
└── packages/
├── package-1/
│ └── node_modules/
│ └── a
├── package-2/
│ └── node_modules/
│ └── b
└── package-3/
└── node_modules/
└── c
但是这会导致依赖存在在不同的 node_modules
中,导致某些使用了 node_modules
的目录结构的包出现异常。比如上面的 a
想找同目录下的 common1
时就找不到了。
Yarn Workspaces vs Lerna
Yarn 作为 NPM 的替代品,带来了很多新的特性,workspaces 就是一个很强大的功能。
简单地说,workspaces 就是 monorepo。
实际上 Yarn workspaces 是对 monorepo 底层依赖处理的一种不同的实现,而且它不需要用户感知,它的定位更加底层。
而 lerna 是一个管理 monorepo 的工具,它提供给用户直接使用的命令集。lerna 既可以基于 NPM 使用,也可以基于 yarn 使用,还可以基于 yarn workspaces 使用。
当使用 yarn workspaces 时,需要在 lerna.json
中指定:
{
"npmClient": "yarn",
"useWorkspaces": true
}
这时通过 lerna bootstrap
初始化依赖后,lerna 会优先把所有的依赖都安装到仓库根目录下的 node_modules
,遇到版本有冲突时,才会把单个项目依赖的版本安装到包目录下的 node_modules
。上面的例子会得到如下结构:
.
├── node_modules/
│ ├── common1
│ ├── common2
│ ├── a
│ ├── b
│ └── c
└── packages/
├── package-1
├── package-2
└── package-3
通过这个方式得到的依赖的目录结构和单独安装时会比较类似,能更好地兼容对 node_modules
目录结构有依赖的包。这样还有一个好处是需要查找 node_modules
中的某个依赖时,更容易找到。
除此之外,使用 Yarn 安装依赖可能速度会快一些,其他方面并没有太大差别。
Yarn workspaces 也可以单独使用,毕竟 lerna 只是在它的基础上进行了封装,具体可以参考官方文档。
总结
Monorepo 在维护一组有关系的包时,可以对多个包同时开发和调试,不仅简化了开发、调试流程,还避免了依赖的重复安装,节约了时间和空间。这得益于 Lerna 或者 Yarn 对 node_modules 的处理以及包集中管理的模式。