使用 lerna 优雅管理多项目
Lin. 切图仔

monorepo管理

对于维护过多个package(功能相近)的同学来说,都会遇到一个选择题,这些package是放在一个仓库里维护还是放在多个仓库里单独维护。Multirepo 是比较传统的做法,即每一个 package 都单独用一个仓库来进行管理。Monorepo 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。

目前各大流行开源项目其实都用了monorepo管理,例如Vue、React、Babel等等。monorepo管理的项目的第一级目录的内容以脚手架为主,主要内容都在 packages目录中、分多个 package 进行管理。

目录结构如下:

1
2
3
4
5
6
7
8
9
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
| ├── pkg3
| | ├── package.json
├── package.json

monorepo 最主要的好处

  1. 统一的工作流
  2. Code Sharing

比如我想看一个 pacakge 的代码、了解某段逻辑,不需要找它的 repo,直接就在当前 repo;当某个需求要修改多个 pacakge 时,不需要分别到各自的 repo 进行修改、测试、发版或者 npm link,直接在当前 repo 修改,统一测试、统一发版。只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package。

lerna

Lerna是一个管理多个 npm 模块的工具,是 Babel 自己用来维护自己的 Monorepo 并开源出的一个项目。优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题。

安装

全局安装lerna

1
npm i -g lerna

初始化项目

1
lerna init

其中 lerna.json & package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// package.json
{
"name": "root",
"private": true, // 私有的,不会被发布,是管理整个项目,与要发布到npm的解耦
"devDependencies": {
"lerna": "^3.15.0"
}
}

// lerna.json
{

"packages": [
// 管理packages目录下所有的package
"packages/*"
],
"version": "0.0.0"
}

创建npm包

1
2
lerna create pkg-1
lerna create pkg-2

在packages目录下新建了pkg-1、pkg-2

新增依赖

1
2
3
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 下的依赖包都提升到工程根目录,来降低安装以及管理的成本。

1
lerna bootstrap --hoist

会发现根目录多了node_modules,里面的依赖就是之前在pkg-1、pkg-2安装的依赖

配置一些命令,方面我们运行依赖提升命令

1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap --hoist"
},
"devDependencies": {
"lerna": "^4.0.0"
}
}

也可以配置lerna,节省 –hoist后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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里面的多余依赖,可以使用相关命令做清理

1
lerna clean

清理前

清理后,node_modules被删除

Lerna + Monorepo 最佳实践

yarn workspace

环境搭建

  • 普通项目:clone下来后通过yarn install,即可搭建完项目,有时需要配合postinstall hooks,来进行自动编译,或者其他设置。

  • monorepo: 各个库之间存在依赖,如A依赖于B,因此我们通常需要将B link到A的node_module里,一旦仓库很多的话,手动的管理这些link操作负担很大,因此需要自动化的link操作,按照拓扑排序将各个依赖进行link

安装yarn

1
npm install yarn --global // 全局安装

配置workspace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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问题

1
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的清理工作

1
2
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安装依赖

对应的三种场景删除依赖如下

1
2
3
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参数可以控制以拓扑排序规则执行命令

1
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的工作,也可以单纯的只做发布操作。

代码提交

commitizen && cz-lerna-changelog

commitizen 是用来格式化 git commit message 的工具,它提供了一种问询式的方式去获取所需的提交信息。

cz-lerna-changelog 是专门为 Lerna 项目量身定制的提交规范,在问询的过程,会有类似影响哪些 package 的选择。如下:

我们使用 commitizencz-lerna-changelog 来规范提交,为后面自动生成日志作好准备。

因为这是整个工程的开发依赖,所以在根目录安装:

1
2
yarn add  -D commitizen
yarn add -D cz-lerna-changelog

安装完成后,在 package.json 中增加 config 字段,把 cz-lerna-changelog 配置给 commitizen。同时因为commitizen不是全局安全的,所以需要添加 scripts 脚本来执行 git-cz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "root",
"private": true,
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-lerna-changelog"
}
},
"devDependencies": {
"commitizen": "^3.1.1",
"cz-lerna-changelog": "^2.0.2",
"lerna": "^3.15.0"
}
}

参考文献

Monorepo最佳实践之Yarn Workspaces

lerna管理前端模块最佳实践

  • 本文标题:使用 lerna 优雅管理多项目
  • 本文作者:Lin.
  • 创建时间:2022-05-18 20:42:02
  • 本文链接:http://www.linzefan.top/hexo-blogs2022/05/18/初探lerna/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论