基于 lerna 的 多项目管理方案
monorepo管理
对于维护过多个package(功能相近)的同学来说,都会遇到一个选择题,这些package是放在一个仓库里维护还是放在多个仓库里单独维护。Multirepo 是比较传统的做法,即每一个 package 都单独用一个仓库来进行管理。Monorepo 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。
目前各大流行开源项目其实都用了monorepo管理,例如Vue、React、Babel等等。monorepo管理的项目的第一级目录的内容以脚手架为主,主要内容都在 packages目录中、分多个 package 进行管理。
目录结构如下:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
| ├── pkg3
| | ├── package.json
├── package.json
monorepo 最主要的好处
- 统一的工作流
- Code Sharing
比如我想看一个 pacakge 的代码、了解某段逻辑,不需要找它的 repo,直接就在当前 repo;当某个需求要修改多个 pacakge 时,不需要分别到各自的 repo 进行修改、测试、发版或者 npm link,直接在当前 repo 修改,统一测试、统一发版。只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package。
lerna
Lerna是一个管理多个 npm 模块的工具,是 Babel 自己用来维护自己的 Monorepo 并开源出的一个项目。优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题。
安装
全局安装lerna
npm i -g lerna
初始化项目
lerna init
其中 lerna.json & package.json
// package.json
{
"name": "root",
"private": true, // 私有的,不会被发布,是管理整个项目,与要发布到npm的解耦
"devDependencies": {
"lerna": "^3.15.0"
}
}
// lerna.json
{
"packages": [
// 管理packages目录下所有的package
"packages/*"
],
"version": "0.0.0"
}
创建npm包
lerna create pkg-1
lerna create pkg-2
在packages目录下新建了pkg-1、pkg-2
lerna-demo
├── README.md
├── lerna.json
├── package.json
└── packages
├── pkg-1
│ ├── README.md
│ ├── __tests__
│ ├── lib
│ ├── node_modules
│ ├── package-lock.json
│ └── package.json
└── pkg-2
├── README.md
├── __tests__
├── lib
├── node_modules
├── package-lock.json
└── package.json
新增依赖
lerna add chalk // 会给所有 package 增加 chalk 依赖
lerna add semver --scope pkg-1 // 单独给 pkg-1 添加semver依赖
lerna add pkg-1 --scope pkg-2 // 添加内部依赖,pkg-2内部依赖pkg-1
依赖包管理
上述1-4步已经包含了 Lerna 整个生命周期的过程了,但当我们维护这个项目时,新拉下来仓库的代码后,需要为各个 package 安装依赖包。
我们在 lerna add 时也发现了,为某个 package 安装的包被放到了这个 package 目录下的 node_modules 目录下。这样对于多个 package 都依赖的包,会被多个 package 安装多次,并且每个 package 下都维护 node_modules ,也不清爽。于是我们使用 --hoist 来把每个 package 下的依赖包都提升到工程根目录,来降低安装以及管理的成本。
lerna bootstrap --hoist
会发现根目录多了node_modules,里面的依赖就是之前在pkg-1、pkg-2安装的依赖
配置一些命令,方面我们运行依赖提升命令
// package.json
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap --hoist"
},
"devDependencies": {
"lerna": "^4.0.0"
}
}
也可以配置lerna,节省 --hoist后缀
// lerna.json
{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
}
},
"version": "0.0.0"
}
// package.json
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap"
},
"devDependencies": {
"lerna": "^4.0.0"
}
}
对于之前被安装在各个package里面的多余依赖,可以使用相关命令做清理
lerna clean
Lerna + Monorepo 最佳实践
yarn workspace
环境搭建
- 普通项目:clone下来后通过yarn install,即可搭建完项目,有时需要配合postinstall hooks,来进行自动编译,或者其他设置。
- monorepo: 各个库之间存在依赖,如A依赖于B,因此我们通常需要将B link到A的node_module里,一旦仓库很多的话,手动的管理这些link操作负担很大,因此需要自动化的link操作,按照拓扑排序将各个依赖进行link
安装yarn
npm install yarn --global // 全局安装
配置workspace
// package.json
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap",
"clean": "lerna clean"
},
// 新增workspaces
"workspaces": [
"packages/pkg-1/*",
"packages/pkg-2/*"
],
"devDependencies": {
"lerna": "^4.0.0"
}
}
运行yarn install,安装依赖,通过使用workspace,yarn install会自动的帮忙解决安装和link问题
yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces
清理环境
在依赖乱掉或者工程混乱的情况下,清理依赖
- 普通项目: 直接删除node_modules以及编译后的产物。
- monorepo: 不仅需要删除root的node_modules的编译产物还需要删除各个package里的node_modules以及编译产物
解决方式:使用lerna clean来删除所有的node_modules,使用yarn workspaces run clean来执行所有package的清理工作
lerna clean # 清理所有的node_modules
yarn workspaces run clean # 执行所有package的clean操作
安装|删除依赖
普通项目: 通过yarn add和yarn remove即可简单解决依赖库的安装和删除问题
monorepo: 一般分为三种场景
- 给某个package安装依赖:yarn workspace packageB add packageA 将packageA作为packageB的依赖进行安装
- 给所有的package安装依赖: 使用yarn workspaces add lodash 给所有的package安装依赖
- 给root 安装依赖:一般的公用的开发工具都是安装在root里,如typescript,我们使用yarn add -W -D typescript来给root安装依赖
对应的三种场景删除依赖如下
yarn workspace packageB remove packageA
yarn workspaces remove lodash
yarn remove -W -D typescript
项目构建
- 普通项目:建立一个build的npm script,使用yarn build即可完成项目构建
- monorepo:区别于普通项目之处在于各个package之间存在相互依赖,如packageB只有在packageA构建完之后才能进行构建,否则就会出错,这实际上要求我们以一种拓扑排序的规则进行构建。
我们可以自己构建拓扑排序规则,很不幸的是yarn的workspace暂时并未支持按照拓扑排序规则执行命令,虽然该 rfc已经被accepted,但是尚未实现, 幸运的是lerna支持按照拓扑排序规则执行命令, --sort参数可以控制以拓扑排序规则执行命令
lerna run --stream --sort build
会执行所有package下的build命令
版本升级及发包
项目测试完成后,就涉及到版本发布,版本发布一般涉及到如下一些步骤
- 条件验证: 如验证测试是否通过,是否存在未提交的代码,是否在主分支上进行版本发布操作
- version_bump:发版的时候需要更新版本号,这时候如何更新版本号就是个问题,一般大家都会遵循 semVer语义,
- 生成changelog: 为了方便查看每个package每个版本解决了哪些功能,我们需要给每个package都生成一份changelog方便用户查看各个版本的功能变化。
- 生成git tag:为了方便后续回滚问题及问题排查通常需要给每个版本创建一个git tag
- git 发布版本:每次发版我们都需要单独生成一个commit记录来标记milestone
- 发布npm包:发布完git后我们还需要将更新的版本发布到npm上,以便外部用户使用
我们发现手动的执行这些操作是很麻烦的且及其容易出错,幸运的是lerna可以帮助我们解决这些问题
yarn官方并不打算支持发布流程,只是想做好包管理工具,因此这部分还是需要通过lerna支持
lerna提供了publish和version来支持版本的升级和发布, publish的功能可以即包含version的工作,也可以单纯的只做发布操作。