1、引言
What Is a Monorepo?: Monorepo is a unified source code repository used by an organisation to host as much of its code as possible.
Monorepo是一种管理团队代码的方式,它摒弃了一个模块一个仓库的方式,而是尽可能地将所有的模块放在一个仓库进行管理。
在团队开发过程中,我们通常会为不同的项目建立多个仓库进行管理,但一旦各个项目间存在相互引用关系,我们每次修改就会产生很大的麻烦,我们以一个Package A包和它的依赖UI Package为例,它们的开发流应该是这样的:

当然,在这一过程中还涉及到仓库权限、publish权限的管理,如果我们的Tom同学想测试Jerry同学正在开发中的UI Package是否符合要求,他必须得拿到一个测试包,npm install到本地才能看到这个效果;如果他还想边测试边console.log看看效果,他只能麻烦Jerry同学在代码里加一下重新发个包。

那么如果我们使用Monorepo模式开发,会有怎样的效果:

我们的Tom同学只要把Jerry同学正在开发的分支拉到本地,自己想怎么写就怎么写。

在开始使用前,先听听缺点
- 随着项目的迭代,在
Monorepo中每个package都会十分庞大,对构建工具是个不小的挑战。(目前已有的方案:例如 Google 的 Bazel, Facebook 的 Buck 和 Twitter 的 Pants,但它们都没有很好的支持JS打包) - 由于项目之间相互依赖,你必须时刻保证良好的代码结构,编译规范以及测试用例。
- 除了对构建工具的挑战外,当项目达到一定规模时,IDE可能会面临崩溃。
当然,诸如Babel、React、Vue-next等等著名的开源项目都在使用Monorepo的方式进行源码管理,此时不上车更待何时。
Babel团队给出了他们的看法: Why is Babel a monorepo?
2、lerna & yarn workspace —— 成熟的Menorepo管理方案
2-1、“芜湖,起飞”——项目初始化
首先让我们使用lerna来初始化项目:
// Step 1
npm install -g lerna
// Step 2
git init learn-lerna
// Step 3
cd learn-lerna && lerna init
运行完之后,我们就看到在learn-lerna/文件夹下会生成如下结构:

其中lerna.json为lerna的配置文件,packages/为项目的存储文件夹。基于对之后自动化构建的规划,我们在根目录下新建一个examples的文件夹,至于这样做的原因及好处,让我们慢慢往下看。
打开lerna.json,添加一些配置:
{
// 定义各个项目存放的位置,这里我们新增一个examples/
"packages": ["packages/*", "examples/*"],
// 当前的版本号
"version": "0.0.0",
/* 以下为新增 */
// 执行命令的client,默认为npm,这里我们需要配置为yarn
"npmClient": "yarn",
// 是否使用workspace工作模式
"useWorkspaces": true
}
对于lerna而言,它的主要功能是版本控制与发布,因此它还需要配合npm、yarn和git一同使用。同时,lerna还支持固定模式(fixed/locked)以及独立模式(independent):
- 固定模式(默认):所有的包共享一个版本号,在每次发布之后会将发布情况记录在根目录下的
changelog中 - 独立模式:每个包各自管理版本
由于UI Package和Utils Package一般为独立发版的,所以我们需要手动将项目调整为独立模式(将配置中的version改成independent即可),如果想了解更多这两个模式的区别,可以参考lerna的官方文档固定模式和独立模式区别。
尽管
lerna也可以进行一些依赖的安装(其实其安装功能会交由yarn或npm处理),但由于它并不能执行构建、测试等任务,因此我们更希望它专注于版本控制与发布,并结合npm和yarn来执行其他任务
接下来打开package.json,配置我们的工作区:
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.22.0"
},
/*新增,定义工作区*/
"workspaces": [
"packages/*",
"examples/*"
]
}
对于yarn而言,它除了作为包管理工具之外,在大型项目的依赖安装时间上会比npm更快,同时yarn在原生程度上就支持Monorepo的包管理模式,这也让我们能更流畅的进行依赖的安装(无论是本地还是远程)。

综上所述,我们推荐使用lerna作为版本发布与控制的管理工具,使用yarn+yarn workspace进行包管理、构建与测试工具,关于这一选择更多的好处,可以参见Why Lerna and Yarn Workspaces is a Perfect Match for Building Mono-Repos?。
2-2、“上下左右BABA”——多包依赖的建立
接下来我们在packegs/中初始化ui以及utils项目,在examples/中初始化blog项目:

然后就该我们的yarn workspace登场了,为各个包添加依赖:
-
添加外部依赖
// 为 blog 添加 react、react-dom 依赖 yarn workspace blog add react react-dom --save -
添加本地依赖
需要注意的是,在添加本地依赖时,我们需要在包后添加版本号,否则
yarn会搜索远程注册表,而不是搜索工作区的包// 为 blog 添加 本地依赖,@zg/ui 为包名 yarn workspace blog add @zg/ui@1.0.0 --save -
添加全局依赖
// 为全局添加 babel dev依赖 yarn add -W babel --dev
删除操作,只需要将add改为remove即可。
经过这些操作后,我们可以发现所有的依赖并没有分别安装在各个项目中,而是将node_modules统一放在根目录下管理,有效避免了依赖重复安装的问题;而我们在本地的依赖则是通过符号链接对应到packages/中的各个项目:

2-3、“将进酒,杯莫停”——构建工具的加入
对于babel的安装与配置,目前网上已经有各种成熟的配置,这里就不再赘述。而对于Monorepo项目而言,我们其实可以利用babel的overrides属性为每个项目进行个性化的配置:
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
// 为每个项目进行个性化配置
"overrides": [
{
"test": ["packages/ui"],
"plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}]
]
},
{
"test": ["packages/utils"],
"plugins": [
["module-resolver", {
"alias": { "~": "./src/scripts" }
}]
]
}
]
}
在前面我们卖了个关子,建立了两个工作区packages/和examples/。相信有同学已经看出了端倪,我们在packages/中存放的是ui和uitls,都是作为静态库进行输出的,而在examples/中,我们则是一个SPA应用blog。由于它们被设计存放在不同的工作区,我们可以针对这一分离利用不同工具进行构建。针对SPA应用,我们需要能支持热模块替换(HMR)的工具来协助我们开发,因此我们可以采用webpack进行打包;而对于静态库的开发,我们选择打包速度更快、压缩体积更小的rollupjs进行构建。
关于更多
rollup和webpack的区别,可以戳一戳这个链接:Rollup:下一代ES模块打包工具。
对于rollupjs和webpack的配置,这里也不在赘述,大家可以自行google学习。(不过rollupjs的打包分析比webpack好看多了,更为直观清晰)
而对于脚本的书写位置,lerna和yarn workspaces给出的建议是存放在根目录的scripts/文件夹中:
2-4、“Mission Complete”——版本更新与发布
-
版本更新——
lerna versionlerna version可以轻松的帮助我们管理packages/中各个包的版本提交记录,同时会在每个包目录下自动生成changelog。首先我们要做的是按照常规提交规范进行
git commit,当然这里更推荐使用git cz进行格式化提交,提交完成之后执行如下命令:// 根据commit提交信息自动生成package版本 lerna version --conventional-commits接下来,
lerna会根据我们的提交记录,按照如下规则进行转换:提交类型 对应版本号转换(版本号为 MAJOR.MINOR.PATCH)fixPATCH发行版 + 1featMINOR发行版 + 1BREAKING CHANGE无论什么类型,都会转换为 MAJOR发行版 + 1
需要注意的是:
-
对于
lerna而言,不论是设定独立模式(independent)还是固定模式(fixed/locked),当某个包发生变化时,其他依赖该包的package都会更新其version。两种模式的不同点在于其MAJOR发行版的号码是否统一。 -
BREAKING CHANGE需要书写在commit的body或者footer的开头部分中,当你执行git commit时,应该按照下面的格式:<type>(<scope>): <subject> // 空一行 <body> // 空一行 <footer>如果使用
git cz提交,可以按照提示自动选择是否有BREAKING CHANGE
-
-
发布——
lerna publish经过了上面的操作,我们已经把修改推送到了远程仓库中,终于,到了这激动人心的时刻。

当然,版权意识不能少,我们需要先选择一个开源许可证,并将对应的
LICENSE安装在项目的根目录下。然后,如果你发布的
scope package,即@xxx/some-package格式的包,需要在每个包的package.json中加入publishConfig:{ "name": "@xxx/some-package", "version": "2.0.0", "main": "index.js", "license": "MIT", /*以下为新增*/ "publishConfig": { //指定范围包的访问权限 不设置public,就付钱升级npm账号吧! "access": "public", //指定发布的目录 可以配合.npmignore "directory": "" } }最后的最后,需要先使用
npm login进行登录,或者用npm whoami判断自己是否登录成功,如果没有问题的话,请庄严的敲下如下的命令:lerna publish from-package注意:如果你使用了
scope,请务必保证你的组织(organizations)已经在NPM中注册,否则你可能会遇到如下的403错误:
如果注册好了,让我们再执行一下
publish指令:
这样我们就成功将工作区内的所有包发布到
NPM上啦。大功告成!🎇🎉🎈🎈🎈🎉🎇
3、总结
Monorepo的概念确实为我们的开发提供了一种新的思路,当优劣所得,还需各位开发者大大斟酌损益。其实对于本文一开始的例子,也可以在Multirepo的基础上通过npm-link来解决,只不过不像lerna + yarn workspaces这样优雅,既有流畅的构建调试流程,又有自动化的changelog与版本更新发布。