MonoRepo最佳实践

monorepo 管理代码有众多好处,但是也带来了很多的技术上的挑战,这里我们在 gitlab 上通过 lerna 和 yarn workspace 结合来管理项目。

由于 yarn 和 lerna 在功能上有较多的重叠,项目中用 yarn 来处理依赖问题,用 lerna 来处理发布问题。一个非 monorepo 的普通项目,常见的开发流程如下,对于 monorepo 项目却可能存在各个问题

搭建环境

主要指安装依赖

普通项目:clone 下来后通过 ​yarn install​,即可搭建完项目,有时需要配合 ​postinstall​ hooks,来进行自动编译,或者其他设置。
monorepo: 各个库之间存在依赖,如 A 依赖于 B,因此我们通常需要将 B link 到 A 的 node_module 里,一旦仓库很多的话,手动的管理这些 link 操作负担很大,因此需要自动化的 link 操作,按照拓扑排序将各个依赖进行 link

解决方式:通过使用 workspace,​yarn install​ 会自动的帮忙解决安装和 link 问题(https://github.com/lerna/lerna/issues/1308)

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

对于安装 local dependency,yarn 的实现暂时有 bug,第一次安装需要指明版本号,否则会安装
失败如下
如果 ui-button 没有发布到 npm
则 yarn workspace ui-form add ui-button 会安装失败,但是
yarn workspace ui-form add ui-button@1.0.0会成功 ,详情见 https://github.com/yarnpkg/yarn/issues/3973

项目构建

普通项目:建立一个​build​的npm script,使用 ​yarn build​ 即可完成项目构建
monorepo:区别于普通项目之处在于各个 package 之间存在相互依赖,如 packageB 只有在 packageA 构建完之后才能进行构建,否则就会出错,这实际上要求我们以一种拓扑排序的规则进行构建。我们可以自己构建拓扑排序规则,很不幸的是 yarn 的 workspace 暂时并未支持按照拓扑排序规则执行命令,虽然该 rfc 已经被 accepted,但是尚未实现.

幸运的是 lerna 支持按照拓扑排序规则执行命令, ​–sort​ 参数可以控制以拓扑排序规则执行命令

1
lerna run --stream --sort build

项目测试

普通项目: 建立一个 ​test​ 的 npm script 即可
monorepo 项目:有两种方式使用统一的 jest 测试配置这样方便全局的跑 jest 即可,好处是可以方便统计所有代码的测试覆盖率,坏处是如果 package 比较异构(如小程序,前端,node 服务端等),统一的测试配置不太好编写每个 package 单独支持 test 命令,使用 ​yarn workspace run test​,坏处是不好统一收集所有代码的测试覆盖率版本升级及发包项目测试完成后,就涉及到版本发布,版本发布一般涉及到如下一些步骤条件验证如验证测试是否通过,是否存在未提交的代码,是否在主分支上进行版本发布操作,以及其他条件更加严苛的一些验证操作可以通过 danger.js,如 rxjs https://github.com/ReactiveX/rxjs/blob/master/dangerfile.js

version_bump

发版的时候需要更新版本号,一般大家都会遵循 semVer 语义,如果版本之间的提交记录较少,能够较为容易的手动更新版本好,但这样也存在人为失误的可能,更好的办法是根据 git 的提交记录自动更新版本号,实际上只要我们的 git commit message 符合 Conventional commit 规范,即可通过工具根据 git 提交记录,更新版本号,简单的规则如下
存在 feat 提交: 需要更新 minor 版本
存在 fix 提交: 需要更新 patch 版本
存在 BREAKING CHANGE 提交: 需要更新大版本

生成 changelog

为了方便查看每个 package 每个版本解决了哪些功能,我们需要给每个 package 都生成一份 changelog 方便用户查看各个版本的功能变化。同理只要我们的 commit 记录符合 conventional commit 规范,即可通过工具为每个 package 生成 changelog 文件

生成 git tag:

为了方便后续回滚问题及问题排查通常需要给每个版本创建一个 git tag

git 发布版本:

每次发版我们都需要单独生成一个 commit 记录来标记 milestone

发布 npm 包:

发布完 git 后我们还需要将更新的版本发布到 npm 上,以便外部用户使用我们发现手动的执行这些操作是很麻烦的且及其容易出错, lerna 可以帮助我们解决这些问题 yarn 官方并不打算支持发布流程,只是想做好包管理工具,因此这部分还是需要通过 lerna 支持
lerna 提供了 ​publish​ 和 ​version​ 来支持版本的升级和发布 publish 的功能可以即包含 version 的工作,也可以单纯的只做发布操作。

lerna version

lerna version 的作用是进行 version bump,支持手动和自动两种模式

只发布某个 package

不支持,lerna 官方不支持仅发布某个 package,见 https://github.com/lerna/lerna/issues/1691,如果需要,只能自己手动的进入package进行发布,这样lerna自带的各种功能就需要手动完成且可能和lerna的功能相互冲突

由于 lerna 会自动的监测 git 提交记录里是否包含指定 package 的文件修改记录,来确定版本更新,这要求设置好合理的 ignore 规则(否则会造成频繁的,无意义的某个版本更新),好处是其可以自动的帮助 package 之间更新版本

例如如果 ui-form 依赖了 ui-button,如果 ui-button 发生了版本变动,会自动的将 ui-form 的对 ui-button 版本依赖更新为 ui-button 的最新版本。 如果 ui-form 发生了版本变动,对 ui-button 并不会造成影响。

自动选择发布版本

使用–conventional-commits 参数会自动的根据 conventional commit 规范和 git commit message 记录帮忙确定更新的版本号。

1
lerna version --conventional-commits

自动确立了版本更新经测试 version_bump 是依赖于文件检测和 subject 结合,并不依赖于 scope,scope 的作用是用来生成 changelog ,即如果是修改了 ui-form 的文件,但是 commit 记录写的是 fix(ui-button),lerna 是会生成 ui-form 的版本更新,并不会去更新 ui-button 的版本

手动选择发布版本

如果 git commit message 发现不太靠谱,且无法修改的话,那么需要手动的确认新版本,version 默认是手动选择版本

1
lerna version

version 成功后会自动的推送到主分支,我一般是关闭主分支的推送权限的,这样就会导致推送失败,但是暂时没找到如何禁止推送主分支的好办法,使用 ​–no-push​ 会把 tag 推送一起禁止掉,好在禁止推送主分支只会报错,但不影响整个流程 lerna version 自动生成的提交格式为“ publish xxx”,并不符合 conventional-commit 规范,因此需要加以修改,我们通过 message 参数可以修改自动生成的提交记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lerna.json
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn",
"command": {
"publish": {
"ignoreChanges": ["*.md"],
"verifyAccess": false,
"verifyRegistry": false,
"message":"chore: publish" // 自定义 version 生成的 message 记录
}
}
}​

changelog.md​

version 完成后会自动生成 changelog.md,但是由于 lerna 是根据什么规则来生成 changelog 的规则尚不清楚,现在发现 A 库的 changlog 里可能包含 B 的 commit 记录

lerna publish

git vesion_bump 完成后,就可以根据 version 生成的 tag 进行 npm 发包了

1
lerna publish from-git

这里没使用 from-package 是因为每次用 from-package 都会在 package.json 里生成个 gitHead 字段,来关联 package 和 git 记录,造成文件被修改,需要手动的 checkout 或者提交掉,暂时没找到方法禁掉这个,下面通过示例来操作下

创建项目

新建项目&&安装 lerna&& 初始化 lerna

1
mkdir monorepo-template && cd monorepo-template && yarn init -y && yarn add -D lerna && lerna init && mkdir packages

lerna 配置使用 yarn workspaces, 使用 independent 模式(根据需求选择是否使用 independent)

1
2
3
4
5
6
7
// lerna.json
{
"packages": ["packages/*"], // 配置 package 目录
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true // 使用 yarn workspaces
}

配置 package.json 使用 yarn workspacess

1
2
3
4
5
6
7
8
// package.json
{
"name": "monorepo-template",
"private": true, // root 禁止发布
"workspaces": [ // 配置 package 目录
"packages/*"
]
}

创建 package
创建 ui-lib 模块
初始化 ui-button 模块

1
cd packages && mkdir ui-lib && yarn init -y

配置

1
2
3
4
5
6
7
8
9
// package.json
{
"name": "ui-button",
"version": "1.0.0",
"main": "index.js",
"publishConfig": {
"access": "publish" // 如果该模块需要发布,对于 scope 模块,需要设置为 publish,否则需要权限验证
}
}

创建 ui-app 模块同上,或者使用 lerna create 快速创建 package

happy hacking~
撒花