基于lerna和yarn workspace的monorepo工作流
先插播一段广告
字节跳动校招提前批开启,欢迎小伙伴们投递简历,提前进入面试环节,提前锁定 offer!,详细的jd可查看 字节跳动校招提前批开始
内推邮箱: yangjian.fe@bytedance.com
标题格式: 校招 /实习-姓名-意向岗位-城市
monorepo管理代码有众多好处,但是也带来了很多的技术上的挑战,github上的很多的项目都是使用lerna管理monorepo项目,我们探讨下在gitlab上也通过lerna和yarn workspace结合来管理项目。由于yarn和lerna在功能上有较多的重叠,我们采用yarn官方推荐的做法,用yarn来处理依赖问题,用lerna来处理发布问题。能用yarn做的就用yarn做吧
一个非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 )
$ 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
对于安装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参数可以控制以拓扑排序规则执行命令
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/rx js/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记录帮忙确定更新的版本号。
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默认是手动选择版本
lerna version
version成功后会自动的推送到主分支,我一般是关闭主分支的推送权限的,这样就会导致推送失败,但是暂时没找到如何禁止推送主分支的好办法,使用--no-push会把tag推送一起禁止掉,好在禁止推送主分支只会报错,但不影响整个流程
lerna version自动生成的提交格式为“ publish xxx",并不符合conventional-commit规范,因此需要加以修改,我们通过message参数可以修改自动生成的提交记录
// 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发包了
lerna publish from-git
这里没使用from-package是因为每次用from-package都会在package.json里生成个gitHead字段,来关联package和git记录,造成文件被修改,需要手动的checkout或者提交掉,暂时没找到方法禁掉这个修改
示例
完整的demo地址 https:// github.com/hardfist/mon orepo-starter
我们通过一个简单的项目来演示上述操作
创建项目
新建项目&&安装lerna&& 初始化lerna
mkdir monorepo-template && cd monorepo-template && yarn init -y && yarn add -D lerna && lerna init && mkdir packages
lerna配置使用yarn workspaces, 使用independent模式(根据需求选择是否使用independent)
// lerna.json
"packages": ["packages/*"], // 配置package目录
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true // 使用yarn workspaces
}
配置package.json使用yarn workspacess
// package.json
"name": "monorepo-template",
"private": true, // root禁止发布
"workspaces": [ // 配置package目录
"packages/*"
}
创建package
创建ui-lib 模块
初始化ui-button模块
cd packages && mkdir ui-lib && yarn init -y
配置
// package.json
"name": "ui-button",
"version": "1.0.0",
"main": "index.js",
"publishConfig": {
"access": "publish" // 如果该模块需要发布,对于scope模块,需要设置为publish,否则需要权限验证
}
创建ui-app 模块
同上,或者使用lerna create快速创建package
lerna create ui-app -y
将ui-lib作为ui-app的依赖
yarn workspace ui-app add ui-lib/1.0.0 # 这里必须加上版本号,否则报错
将lodash添加为所有package的依赖(不包含root)
yarn workspaces run add lodash
将typescript设置为root的开发依赖
一般root只包含一些开发工具依赖如webpack,babel,typescript等
yarn add -W -D typescript jest
构建和测试
为ui-lib和ui-app添加build和test和clean脚本
- ui-lib
packages/ui-lib/package.json
"scripts": {
"build": "tsc",
"test": "jest",
"clean": "rimraf lib"
}
- ui-app
packages/ui-app/package.json
"scripts": {
"test": "echo success", // 如果对应脚本无事可做,可以直接echo success
"build": "tsc",
"clean": "rimraf lib"
}
在root里添加build、test和clean脚本
"scripts": {
"build": "lerna run --stream --sort build", // 按照拓扑依赖进行构建
"clean": "yarn workspaces run clean", // 彼此独立,可以并行执行
"test": "yarn workspaces run test" // 彼此独立可以并行执行
},
添加conventional-commit支持
lerna的version_bump和changelog生成都依赖于conventional-commit,因此需要保证commit-msg符合规范。
添加@commitlint/cli和@commitlint/config-conventional以及husky
yarn add -W -D @commitlint/cli @commitlint/conventional-commit lint-staged husky
配置commitlint
// commmitlint.config.js
module.exports = {
extends: [
"@commitlint/config-conventional"
};
配置commit-msg的hooks
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
添加git-cz,支持commit-msg提示
$ yarn add -W -D commitizen cz-conventional-changelog
配置commitizen并添加commit为npm script
// package.json
"scripts": {
commit: "git-cz"
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
这样后续commit,就可以使用yarn commit进行commit,其会自动做出如下提示
其自动生成的commit-msg如下
发版
开发测试通过后,每隔一段时间即可发版,我们使用lerna version来做发版
配置发版的message
// lerna.json
"packages": ["packages/*"],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"ignoreChanges": ["*.md"], // md文件更新,不触发版本变动
"verifyAccess": false, // 内网发包需开启
"verifyRegistry": false, // 内网发包需开启
"message": "chore: publish" // 修改默认的publish的commit msg
}
``
配置发版的策略,我们积极convention-commit来发版
// package.json
"scripts":
version: "lerna version --conventional-commits" ## 生成changelog文件以及根据commit来进行版本变动
}
发版
$ yarn run version # 不要使用 yarn version,yarn version 是yarn自动的命令不是npm script
这个会提示用户输入版本,如果不想这个提示可以关闭
// package.json