We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
专栏上篇文章传送门:在本地和CI/CD中支持npm免登录发布
本节涉及的内容源码可在vue-pro-components c9 分支找到,欢迎 star 支持!
本文是 基于Vite+AntDesignVue打造业务组件库 专栏第 10 篇文章【你知道怎么组织和优化前端研发流程吗?】,前面几篇都在说函数库开发的相关内容,所以本文接着围绕这块说,主要是把研发流程梳理清楚,方便后续更多内容的铺开。
我们先粗略整理一下函数库的主要研发流程。
以上是主要流程,其他的辅助事项可以按需穿插,比如提交代码前是不是经过 husky, eslint, prettier, stylelint, commitlint 等。
第一步显然是跟工具无关的,纯粹是开发者自己写代码。
先说重点,第三步是修改版本号,我们来分情况讨论一下。
如果是单包工程,其实只有一个版本号要管理,第一种方式是手动改版本号,不借助任何工具,就相当于把第三步的修改版本号与第一步的写代码放在一起做了。第二种方式是让工具去决定版本号,但工具怎么知道你期望的版本号是什么呢?这就必须先有规范。
首先要有版本号的规范,有了版本号规范才能知道下个版本号有哪些选择,这对应 Semver(Semantic Versioning)规范。
接下来还要有一套规范,能根据用户的输入或者操作推导出下一个 Semver 版本号。
一种做法是使用 npm version 命令,它支持 major/minor/patch 等版本更新操作,还支持通过钩子把 changelog 和后续的自动化流程全部做了,我之前有写过一篇前端自动化部署的深度实践中有提到,大家可以参考着看看。但是这还是需要我们自己决定到底是 major/minor/patch 的哪一种版本更新,无法完全自动化。
还有一种做法是基于 Git Commit 来实现自动化推导版本号,只要我们的 commit 符合 Conventional Commits 规范,通过分析两个版本之间的所有 commit 信息,就有机会推导出下一个版本号。
按照上图中提供的信息,我们可以知道,fix 类型的 commit 关联着 patch 位的版本号更新,feat 关联着 minor 位的版本号更新,Breaking CHANGE(具体实现是在 type(scope) 后接!,或者在 message footer 中使用Breaking CHANGE: )关联着 major 位的版本号更新。
!
Breaking CHANGE:
基于此,一些自动化工具也应运而生,比如基于 Conventional Commits 生成 changelog 的底层 API —— conventional-changelog,以及一些上层工具 standard-version, semantic-release,还有我们相对熟悉的 commitizen + cz-conventional-changelog + husky + commitlint + npm version + conventional-changelog-cli 组合拳。
在单包工程中,适当选择以上部分工具已经足够自动我们推导出下一个版本号了。而在 monorepo 工程中会存在多个子包,多个子包的版本号如何确定呢?
以 lerna 为例,有两种版本策略,具体见组件库技术选型和开发环境搭建文中相关介绍。如果我们采用 Fixed Mode,也就是 monorepo 工程中各个子包都共用一个版本号,那事情就简单得多,因为这跟单包工程没什么差别,只要根据 git commit message 简单推导出下个版本号即可。
如果我们选择 Independent Mode,也就是各个子包采用独立的版本号,那么 version bump 这件事情就变得复杂起来,因为我们在一次 commit 中可能不止修改了一个子包(毕竟是人为操作),产生耦合的几率比较大,版本界限不是很清晰。一次 commit 到底对应哪个子包的版本,谁都不好说清楚,因为我们得分析每次 commit 到底修改了哪些文件才能得出结论。
还好 lerna version 已经支持这个能力,只要我们执行下面的命令:
lerna version --conventional-commits --yes
lerna 就会遵循 Conventional Commits 规范,自动帮我们进行 version bump,生成相关的 CHANGELOG.md 文件。
说完最重要的版本号问题,我们再回到第二步,第二步是 commit,commit 环节可以穿插一些工具。
我们先补齐一些代码校验脚本,便于在合适的时间调用。
代码校验主要是通过 eslint 和 stylelint 完成,prettier 则是以插件的形式存在,被 eslint 和 stylelint 调用。
"lint": "eslint packages --cache --ext .js,.mjs,.jsx,.ts,.tsx,.vue", "lint-fix": "eslint packages --cache --fix --ext .js,.mjs,.jsx,.ts,.tsx,.vue", "lint-style": "stylelint packages/**/src/**/*.{vue,css,less} --cache", "lint-style-fix": "stylelint packages/**/src/**/*.{vue,css,less} --cache --fix"
主要脚本如上所示,其中lint只负责 lint,不进行 fix;lint-fix会在 lint 时顺手修复问题;lint-style和lint-style-fix同理。
lint
lint-fix
lint-style
lint-style-fix
我们期望在提交代码前进行代码质量校验,这需要用到 git hooks 中的 pre-commit 钩子,在 pre-commit 钩子中可以执行 eslint 等 lint 命令。
husky 对 git hooks 进行了良好的封装,我们根据指引安装一下。
// 由于我们当前使用的是 Yarn 1,所以可以执行以下命令安装 npx husky-init && yarn
按道理,我们只要新增一个 pre-commit 钩子,执行相关的 lint 命令即可。但是,每次 commit 都 lint 整个工程的文件是比较浪费时间的,所以我们可以再引入一个 lint-staged 进行优化,lint-staged 只会 lint 进入了 staged 状态的文件,这样效率就比较高。
// 安装依赖 yarn add -DW lint-staged
lint-staged 通过配置文件决定具体要对哪些文件执行哪些脚本,我们新建一个lint-staged.config.js配置文件。
lint-staged.config.js
module.exports = { "packages/**/src/**/*.{js,mjs,jsx,ts,tsx,vue}": "eslint --cache --fix", "packages/**/src/**/*.{css,less,vue}": "stylelint --cache --fix", };
接着把.husky/pre-commit文件的内容改为:
.husky/pre-commit
#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged $1
所以,完整的逻辑是:
首先安装一下 commitizen 及相关依赖:
yarn add -DW commitizen cz-conventional-changelog
然后在package.json中加入以下配置:
package.json
"config": { "commitizen": { "path": "cz-conventional-changelog" } }
接着就可以正常使用git cz命令了。
git cz
但是,即便引入了 commitizen,我们也不能保证开发者一定会使用 git cz 来规范自己的行为,所以我们可以再利用 git 的 commit-msg 钩子,再配合 commitlint 验证开发者提交的 commit 信息。
yarn add -DW @commitlint/config-conventional @commitlint/cli
新增一个配置文件:
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
接着在.husky目录下新增一个commit-msg钩子。
.husky
commit-msg
#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx --no-install commitlint --edit $1
此时不规范的 commit 就是无法通过的。
我们再来回顾和梳理一下流程:
2 ~ 4 都是提交代码时触发的,针对第 5 步可以单独写个 script,比如:
"bump-version": "lerna version --conventional-commits --create-release github --yes",
第 6 步是通过 github actions yaml 文件配置的,执行的主要脚本就是打包构建以及发布到 npm。
"release:ci": "yarn buildBatch && yarn publish:package",
针对 github actions 的触发条件,我优先考虑的是 github release 的创建。
on: release: types: [created]
lerna version --create-release github 命令的执行会触发 github actions workflow。
lerna version --create-release github
但是在使用的过程中,我也发现一个问题,lerna version 不仅会修改真正发生内容变化的子包的版本号,还会修改 workspaces 中引用了这个子包的其他子包的版本号。
这样说可能也不好理解,举例说明一下。
假设我在一次开发过程中仅仅给@vue-pro-components/utils加了一个功能,在执行 lerna version 命令时,它的版本号minor位会加 1,这合情合理;
@vue-pro-components/utils
minor
由于vue-pro-components以及@vue-pro-components/headless这两个包都引用了@vue-pro-components/utils,所以它们俩的package.json中的依赖@vue-pro-components/utils的版本号也会升一级,因此它们俩自身的版本号也会随之更新。
vue-pro-components
@vue-pro-components/headless
此时会生成三个 tag,并发布三个 github release,分别是@vue-pro-components/[email protected], @vue-pro-components/[email protected], [email protected]。
@vue-pro-components/[email protected]
[email protected]
其实创建三个 release 也没啥问题,因为我采用的是 lerna 的 Independent Mode,各个子包版本号独立;但是考虑到我的 github actions 的触发条件是release 的创建,这也意味着三个 release 会触发三次 github actions workflow,虽然三次 workflow 执行完的结果是一样的,但是完全没必要做重复的工作,按需使用是我们的宗旨。
目前,lerna version 这个行为还没有参数可以用来控制开关,但是并不是说这是 lerna 的问题,也许我们可以改进自己的流程来规避这个问题。
接上面,我的第一个想法是,在不同的 release 对应的 workflow 中取出 release name,release name 中有包名的信息,自然就可以基于此按需打包发布。
但是,这也存在一个问题,实际上,包之间是有依赖关系的,也就意味着在某些工序上可能有先后顺序。
如果所有子包都各自独立打包,其实是有问题的,比如当多个 release 对应的 workflow 同时进行时,如果包 A 依赖的某个包 B 还没打包并发布到 npm registry,就有可能导致 A 打包出错。
所以最好的办法还是按依赖关系决定的顺序,放在一起打包发布。
我的第二个想法是:执行 lerna version 的时候不要创建 release,也就是不带--create-release参数。接着再通过其他脚本或工具给整个工程打个 tag 和 release。这样一来,一次发布过程就只会产生一个 release,因此也只会执行一次 github actions workflow,看起来还比较符合我的心意。
--create-release
我们考虑引入 release-it,用它来重新组织流程。
我们这里用到了一个插件 @release-it/conventional-changelog,它很重要。
我们再理一遍流程:
before:init
packages-bump-version
lerna version --conventional-commits --no-private --yes
其实就是在原来的基础上去掉了--create-release github。执行这条命令会更新 packages 目录下各个包的版本号,并为各个子包更新 CHANGELOG.md 文件。
--create-release github
getChangelog
getIncrementedVersionCI
release
afterRelease
bump
npm version
beforeRelease
试用了上面的流程之后,总体感觉还好,没什么明显问题,但是我发现根目录下 CHANGELOG.md 的生成不符合我的直觉。
由于我在 0.2.0 版本中提交了一个 feat 类型的 commit,相关的 Features 记录应该要体现到 CHANGELOG.md 中,但是结果并没有。
我发现这是因为 lerna version 虽然去掉了--create-release参数,没有再创建 release,但是 tag 还是打出来了。这就会导致 release-it 在对比 0.2.0 和 @vue-pro-components/[email protected] 两个 tag 的差异时,找出的 commits 只是 chore 类型的 release 说明,比如:
0.2.0
chore: release v0.2.0
chore类型的 commit 记录不足以体现到 CHANGELOG.md 中,这与 Conventional Changelog Configuration Spec 有关。
所以要想办法去掉 lerna version 创建 tag 的行为。
我查了一下 lerna version 的文档,发现有一个参数--no-git-tag-version看起来比较贴合我的需求,用了一下发现,它的行为是既不提交 commit,也不打 tag。而不做 commit 就会导致 git 工作区不是 clean 状态,这会导致后续的 release-it 流程无法继续。release-it 也有个配置项git.requireCleanWorkingDir可以关闭 git 工作区 clean 的检查,不过我暂时不打算这么做。
--no-git-tag-version
git.requireCleanWorkingDir
我的思路是:由于我的目的还是去掉 lerna version 创建 tag 的行为,所以还是要使用 --no-git-tag-version这个参数,但是我紧接着会自行执行一次 commit,用于保持 git 工作区的 clean 状态。所以我把关键脚本改为下面这样了:
"packages-bump-version": "lerna version --conventional-commits --no-git-tag-version --no-push --no-private --yes", "commit-packages-version-info": "git add . && git commit -m \"chore: bump packages version\"", "determine-packages-version": "yarn packages-bump-version && yarn commit-packages-version-info",
release-it 的before:init钩子执行的脚本变成:
"before:init": "yarn determine-packages-version",
这就对应我上面说的思路,把一个完整的脚本拆成两个,第一个还是调用 lerna version,第二个变成调用我自己定义的 git add 以及 git commit 命令,基于此绕过创建 tag 的行为。
按这个流程工作,根目录下生成的 CHANGELOG.md 变得正常,但是......
当我以为万事大吉时,却发现,按照这个方案实践时,虽然根目录的 CHANGELOG.md 正常了,但是各个子包中的 version bump 以及 CHANGELOG.md 都变得不正常了,我们来分析一下 lerna version 为什么会出问题。
由于我加上了--no-git-tag-version参数,这就会导致 lerna version 不会为各个子包打上特殊的类似于@vue-pro-components/[email protected]的 tag,这会引起一些问题。为了搞清楚问题原因,我们来分析一下流程。
经过 debug 发现,lerna version 会经过一些关键的节点。
getVersionsForUpdates
recommendVersions
recommendVersion
gitSemverTags
gitRawCommits
conventionalCommitsParser
其中gitSemverTags决定了程序会查询哪些 tags,这里有一个关键的lernaTag函数,执行完lernaTag函数后,tags的值是一个空数组,这是因为我们放弃了打特殊 tag。
lernaTag
tags
如果tags是一个空数组,就会影响后面的gitRawCommits函数的执行,导致from参数是空的,这就意味着程序会读取整个 git log。
from
这就意味着:不管我最近一次改的是什么内容,只要 git log 的历史记录中有 Breaking Change,下个版本号就会是大版本更新,同理,只要 git log 中有 feat 类型的 commit,下个版本号就会更新 minor 位。同时子包每个版本的 CHANGELOG.md 都是“大而全”。
这基本上就崩盘了,版本号都不对了。看来针对各个子包的特殊 tag 还是不能少,否则 lerna 也无法正确分析出下个版本号,所以--no-git-tag-version还是不能加,但是去掉又会发生上一节说的问题,怎么想办法解决一下呢?
我的思路是:release-it 能不能只分析0.0.0这种格式的 tag 之间的差异,因为这种格式的 tag 是 release-it 针对整个工程打的,分析这两个 tag 之间的 commit 肯定是能够正确反映出整个工程的版本更新情况的。
0.0.0
经过调试发现,核心的问题在于getChangelogStream方法中,latestTag的值为 lerna version 针对某个子包打出的 tag,能不能想办法让他变成 2.1.0 呢?
getChangelogStream
latestTag
2.1.0 是调试时整个工程的版本号。
首先是要找到latestTag是在哪里被赋值的,自然是优先在 release-it 的一些核心插件中去找,很快能找到目标模块 GitBase.js,其中的getLatestTagName方法决定了什么样的 tag 会作为候选目标。
getLatestTagName
这里有一行很关键的代码:
git describe --tags --match=${match} --abbrev=0
--match是我们能通过参数tagMatch控制的,我们参照配置清单release-it.json把git.tagMatch配置好,仅匹配数字开头的 tag 即可。
--match
tagMatch
git.tagMatch
经过这波优化,根目录和子包中的 CHANGELOG.md 都能正确地生成,也算是成功地把 lerna 和 release-it 结合起来了!
踩过上面几个坑后,咱们总结出来的流程基本上能应付简单的 monorepo 使用场景,但是也并非说就没有问题了。我遇到的一个很高频的问题就是:由于创建 release 的过程需要多次与 github 交互,这就涉及到国内比较经典的网络问题,可能会出现 lerna version 成功了,但是 release-it 的某个步骤与 github 失联的情况。release-it 会在失败后执行一些回滚操作,而 lerna version 脚本是在钩子中被执行的,release-it 并不会回滚这部分自定义的脚本,这就会导致回滚不彻底。
不过这也是后话了,后面再说说怎么解决这个问题。
在 debug 的过程中还学到了一些细节。
当主版本号为 0 时,所有的变更都认为是不稳定的,此时即便是我们在 commit 信息中包含了 BREAKING CHANGE,lerna version 也不会为我们修改 major 版本号,具体请看下图:
the transition from 0.x to 1.x must be explicitly requested by the user. Breaking changes MUST NOT automatically bump the major version from 0.x to 1.x.
the transition from 0.x to 1.x must be explicitly requested by the user.
Breaking changes MUST NOT automatically bump the major version from 0.x to 1.x.
所以,如果你遇到的问题符合上述情况,请不必怀疑自己,0.x 版本到 1.x 版本的变更必须由你自行操作,工具不负责这个场景。没有实践过还真不知道这个细节!
通过本文的学习,我们不仅能掌握如何组织起经典的前端研发流程,还能认识到,优秀的工具也不是拍脑袋想出来的,一定是先有规范,再根据规范出上层工具,所以制定规范是一件很重要的事情。另外一点就是,不要局限于开源工具提供的能力,可以自己适当地去想办法优化或者改造,以达到自己的目的。
当然,文中所述流程不一定适合所有场景,仅供读者参考!
如果您对我的专栏感兴趣,欢迎您订阅关注本专栏,接下来可以一同探讨和交流组件库开发过程中遇到的问题。
技术交流&闲聊:程序员白彬
The text was updated successfully, but these errors were encountered:
No branches or pull requests
专栏上篇文章传送门:在本地和CI/CD中支持npm免登录发布
本节涉及的内容源码可在vue-pro-components c9 分支找到,欢迎 star 支持!
本文是 基于Vite+AntDesignVue打造业务组件库 专栏第 10 篇文章【你知道怎么组织和优化前端研发流程吗?】,前面几篇都在说函数库开发的相关内容,所以本文接着围绕这块说,主要是把研发流程梳理清楚,方便后续更多内容的铺开。
梳理研发流程
我们先粗略整理一下函数库的主要研发流程。
以上是主要流程,其他的辅助事项可以按需穿插,比如提交代码前是不是经过 husky, eslint, prettier, stylelint, commitlint 等。
版本号处理
第一步显然是跟工具无关的,纯粹是开发者自己写代码。
先说重点,第三步是修改版本号,我们来分情况讨论一下。
如果是单包工程,其实只有一个版本号要管理,第一种方式是手动改版本号,不借助任何工具,就相当于把第三步的修改版本号与第一步的写代码放在一起做了。第二种方式是让工具去决定版本号,但工具怎么知道你期望的版本号是什么呢?这就必须先有规范。
首先要有版本号的规范,有了版本号规范才能知道下个版本号有哪些选择,这对应 Semver(Semantic Versioning)规范。
接下来还要有一套规范,能根据用户的输入或者操作推导出下一个 Semver 版本号。
一种做法是使用 npm version 命令,它支持 major/minor/patch 等版本更新操作,还支持通过钩子把 changelog 和后续的自动化流程全部做了,我之前有写过一篇前端自动化部署的深度实践中有提到,大家可以参考着看看。但是这还是需要我们自己决定到底是 major/minor/patch 的哪一种版本更新,无法完全自动化。
还有一种做法是基于 Git Commit 来实现自动化推导版本号,只要我们的 commit 符合 Conventional Commits 规范,通过分析两个版本之间的所有 commit 信息,就有机会推导出下一个版本号。
按照上图中提供的信息,我们可以知道,fix 类型的 commit 关联着 patch 位的版本号更新,feat 关联着 minor 位的版本号更新,Breaking CHANGE(具体实现是在 type(scope) 后接
!
,或者在 message footer 中使用Breaking CHANGE:
)关联着 major 位的版本号更新。基于此,一些自动化工具也应运而生,比如基于 Conventional Commits 生成 changelog 的底层 API —— conventional-changelog,以及一些上层工具 standard-version, semantic-release,还有我们相对熟悉的 commitizen + cz-conventional-changelog + husky + commitlint + npm version + conventional-changelog-cli 组合拳。
在单包工程中,适当选择以上部分工具已经足够自动我们推导出下一个版本号了。而在 monorepo 工程中会存在多个子包,多个子包的版本号如何确定呢?
以 lerna 为例,有两种版本策略,具体见组件库技术选型和开发环境搭建文中相关介绍。如果我们采用 Fixed Mode,也就是 monorepo 工程中各个子包都共用一个版本号,那事情就简单得多,因为这跟单包工程没什么差别,只要根据 git commit message 简单推导出下个版本号即可。
如果我们选择 Independent Mode,也就是各个子包采用独立的版本号,那么 version bump 这件事情就变得复杂起来,因为我们在一次 commit 中可能不止修改了一个子包(毕竟是人为操作),产生耦合的几率比较大,版本界限不是很清晰。一次 commit 到底对应哪个子包的版本,谁都不好说清楚,因为我们得分析每次 commit 到底修改了哪些文件才能得出结论。
还好 lerna version 已经支持这个能力,只要我们执行下面的命令:
lerna 就会遵循 Conventional Commits 规范,自动帮我们进行 version bump,生成相关的 CHANGELOG.md 文件。
husky + lint
说完最重要的版本号问题,我们再回到第二步,第二步是 commit,commit 环节可以穿插一些工具。
我们先补齐一些代码校验脚本,便于在合适的时间调用。
代码校验主要是通过 eslint 和 stylelint 完成,prettier 则是以插件的形式存在,被 eslint 和 stylelint 调用。
主要脚本如上所示,其中
lint
只负责 lint,不进行 fix;lint-fix
会在 lint 时顺手修复问题;lint-style
和lint-style-fix
同理。我们期望在提交代码前进行代码质量校验,这需要用到 git hooks 中的 pre-commit 钩子,在 pre-commit 钩子中可以执行 eslint 等 lint 命令。
husky 对 git hooks 进行了良好的封装,我们根据指引安装一下。
按道理,我们只要新增一个 pre-commit 钩子,执行相关的 lint 命令即可。但是,每次 commit 都 lint 整个工程的文件是比较浪费时间的,所以我们可以再引入一个 lint-staged 进行优化,lint-staged 只会 lint 进入了 staged 状态的文件,这样效率就比较高。
lint-staged 通过配置文件决定具体要对哪些文件执行哪些脚本,我们新建一个
lint-staged.config.js
配置文件。接着把
.husky/pre-commit
文件的内容改为:所以,完整的逻辑是:
规范 commit
首先安装一下 commitizen 及相关依赖:
然后在
package.json
中加入以下配置:接着就可以正常使用
git cz
命令了。但是,即便引入了 commitizen,我们也不能保证开发者一定会使用 git cz 来规范自己的行为,所以我们可以再利用 git 的 commit-msg 钩子,再配合 commitlint 验证开发者提交的 commit 信息。
新增一个配置文件:
接着在
.husky
目录下新增一个commit-msg
钩子。此时不规范的 commit 就是无法通过的。
回顾流程
我们再来回顾和梳理一下流程:
2 ~ 4 都是提交代码时触发的,针对第 5 步可以单独写个 script,比如:
第 6 步是通过 github actions yaml 文件配置的,执行的主要脚本就是打包构建以及发布到 npm。
针对 github actions 的触发条件,我优先考虑的是 github release 的创建。
但是在使用的过程中,我也发现一个问题,lerna version 不仅会修改真正发生内容变化的子包的版本号,还会修改 workspaces 中引用了这个子包的其他子包的版本号。
这样说可能也不好理解,举例说明一下。
假设我在一次开发过程中仅仅给
@vue-pro-components/utils
加了一个功能,在执行 lerna version 命令时,它的版本号minor
位会加 1,这合情合理;由于
vue-pro-components
以及@vue-pro-components/headless
这两个包都引用了@vue-pro-components/utils
,所以它们俩的package.json
中的依赖@vue-pro-components/utils
的版本号也会升一级,因此它们俩自身的版本号也会随之更新。此时会生成三个 tag,并发布三个 github release,分别是
@vue-pro-components/[email protected]
,@vue-pro-components/[email protected]
,[email protected]
。其实创建三个 release 也没啥问题,因为我采用的是 lerna 的 Independent Mode,各个子包版本号独立;但是考虑到我的 github actions 的触发条件是release 的创建,这也意味着三个 release 会触发三次 github actions workflow,虽然三次 workflow 执行完的结果是一样的,但是完全没必要做重复的工作,按需使用是我们的宗旨。
目前,lerna version 这个行为还没有参数可以用来控制开关,但是并不是说这是 lerna 的问题,也许我们可以改进自己的流程来规避这个问题。
改进流程
idea 1
接上面,我的第一个想法是,在不同的 release 对应的 workflow 中取出 release name,release name 中有包名的信息,自然就可以基于此按需打包发布。
但是,这也存在一个问题,实际上,包之间是有依赖关系的,也就意味着在某些工序上可能有先后顺序。
如果所有子包都各自独立打包,其实是有问题的,比如当多个 release 对应的 workflow 同时进行时,如果包 A 依赖的某个包 B 还没打包并发布到 npm registry,就有可能导致 A 打包出错。
所以最好的办法还是按依赖关系决定的顺序,放在一起打包发布。
idea 2
我的第二个想法是:执行 lerna version 的时候不要创建 release,也就是不带
--create-release
参数。接着再通过其他脚本或工具给整个工程打个 tag 和 release。这样一来,一次发布过程就只会产生一个 release,因此也只会执行一次 github actions workflow,看起来还比较符合我的心意。我们考虑引入 release-it,用它来重新组织流程。
我们这里用到了一个插件 @release-it/conventional-changelog,它很重要。
我们再理一遍流程:
before:init
钩子执行packages-bump-version
命令,packages-bump-version
命令对应:其实就是在原来的基础上去掉了
--create-release github
。执行这条命令会更新 packages 目录下各个包的版本号,并为各个子包更新 CHANGELOG.md 文件。getChangelog
和getIncrementedVersionCI
方法,结合起来决定了下一个版本号,具体到内部逻辑,其核心是用到了 conventional-recommended-bump 这个包,它能基于 conventional commits 规范给出建议的 releaseType(对应 major, minor, patch 等),再结合 semver.inc,就能得到下个版本号。release
和afterRelease
钩子。其中核心插件 npm 执行了关键的bump
钩子,通过npm version
更新了 package.json 文件中的 version 字段;插件 @release-it/conventional-changelog 用到了beforeRelease
钩子来生成 CHANGELOG.md,其中用到了我们在上面提到的 conventional-changelog 这个基础包。CHANGELOG.md 不符合直觉
试用了上面的流程之后,总体感觉还好,没什么明显问题,但是我发现根目录下 CHANGELOG.md 的生成不符合我的直觉。
由于我在 0.2.0 版本中提交了一个 feat 类型的 commit,相关的 Features 记录应该要体现到 CHANGELOG.md 中,但是结果并没有。
我发现这是因为 lerna version 虽然去掉了
--create-release
参数,没有再创建 release,但是 tag 还是打出来了。这就会导致 release-it 在对比0.2.0
和@vue-pro-components/[email protected]
两个 tag 的差异时,找出的 commits 只是 chore 类型的 release 说明,比如:chore类型的 commit 记录不足以体现到 CHANGELOG.md 中,这与 Conventional Changelog Configuration Spec 有关。
所以要想办法去掉 lerna version 创建 tag 的行为。
我查了一下 lerna version 的文档,发现有一个参数
--no-git-tag-version
看起来比较贴合我的需求,用了一下发现,它的行为是既不提交 commit,也不打 tag。而不做 commit 就会导致 git 工作区不是 clean 状态,这会导致后续的 release-it 流程无法继续。release-it 也有个配置项git.requireCleanWorkingDir
可以关闭 git 工作区 clean 的检查,不过我暂时不打算这么做。我的思路是:由于我的目的还是去掉 lerna version 创建 tag 的行为,所以还是要使用
--no-git-tag-version
这个参数,但是我紧接着会自行执行一次 commit,用于保持 git 工作区的 clean 状态。所以我把关键脚本改为下面这样了:release-it 的
before:init
钩子执行的脚本变成:这就对应我上面说的思路,把一个完整的脚本拆成两个,第一个还是调用 lerna version,第二个变成调用我自己定义的 git add 以及 git commit 命令,基于此绕过创建 tag 的行为。
按这个流程工作,根目录下生成的 CHANGELOG.md 变得正常,但是......
顾此失彼
当我以为万事大吉时,却发现,按照这个方案实践时,虽然根目录的 CHANGELOG.md 正常了,但是各个子包中的 version bump 以及 CHANGELOG.md 都变得不正常了,我们来分析一下 lerna version 为什么会出问题。
由于我加上了
--no-git-tag-version
参数,这就会导致 lerna version 不会为各个子包打上特殊的类似于@vue-pro-components/[email protected]
的 tag,这会引起一些问题。为了搞清楚问题原因,我们来分析一下流程。经过 debug 发现,lerna version 会经过一些关键的节点。
getVersionsForUpdates
以及recommendVersions
。recommendVersion
。gitSemverTags
。gitRawCommits
。conventionalCommitsParser
。其中
gitSemverTags
决定了程序会查询哪些 tags,这里有一个关键的lernaTag
函数,执行完lernaTag
函数后,tags
的值是一个空数组,这是因为我们放弃了打特殊 tag。如果
tags
是一个空数组,就会影响后面的gitRawCommits
函数的执行,导致from
参数是空的,这就意味着程序会读取整个 git log。这就意味着:不管我最近一次改的是什么内容,只要 git log 的历史记录中有 Breaking Change,下个版本号就会是大版本更新,同理,只要 git log 中有 feat 类型的 commit,下个版本号就会更新 minor 位。同时子包每个版本的 CHANGELOG.md 都是“大而全”。
这基本上就崩盘了,版本号都不对了。看来针对各个子包的特殊 tag 还是不能少,否则 lerna 也无法正确分析出下个版本号,所以
--no-git-tag-version
还是不能加,但是去掉又会发生上一节说的问题,怎么想办法解决一下呢?我的思路是:release-it 能不能只分析
0.0.0
这种格式的 tag 之间的差异,因为这种格式的 tag 是 release-it 针对整个工程打的,分析这两个 tag 之间的 commit 肯定是能够正确反映出整个工程的版本更新情况的。经过调试发现,核心的问题在于
getChangelogStream
方法中,latestTag
的值为 lerna version 针对某个子包打出的 tag,能不能想办法让他变成 2.1.0 呢?首先是要找到
latestTag
是在哪里被赋值的,自然是优先在 release-it 的一些核心插件中去找,很快能找到目标模块 GitBase.js,其中的getLatestTagName
方法决定了什么样的 tag 会作为候选目标。这里有一行很关键的代码:
--match
是我们能通过参数tagMatch
控制的,我们参照配置清单release-it.json把git.tagMatch
配置好,仅匹配数字开头的 tag 即可。经过这波优化,根目录和子包中的 CHANGELOG.md 都能正确地生成,也算是成功地把 lerna 和 release-it 结合起来了!
遗留问题
踩过上面几个坑后,咱们总结出来的流程基本上能应付简单的 monorepo 使用场景,但是也并非说就没有问题了。我遇到的一个很高频的问题就是:由于创建 release 的过程需要多次与 github 交互,这就涉及到国内比较经典的网络问题,可能会出现 lerna version 成功了,但是 release-it 的某个步骤与 github 失联的情况。release-it 会在失败后执行一些回滚操作,而 lerna version 脚本是在钩子中被执行的,release-it 并不会回滚这部分自定义的脚本,这就会导致回滚不彻底。
不过这也是后话了,后面再说说怎么解决这个问题。
细节
在 debug 的过程中还学到了一些细节。
主版本号为0,BREAKING CHANGE 无效
当主版本号为 0 时,所有的变更都认为是不稳定的,此时即便是我们在 commit 信息中包含了 BREAKING CHANGE,lerna version 也不会为我们修改 major 版本号,具体请看下图:
所以,如果你遇到的问题符合上述情况,请不必怀疑自己,0.x 版本到 1.x 版本的变更必须由你自行操作,工具不负责这个场景。没有实践过还真不知道这个细节!
结语
通过本文的学习,我们不仅能掌握如何组织起经典的前端研发流程,还能认识到,优秀的工具也不是拍脑袋想出来的,一定是先有规范,再根据规范出上层工具,所以制定规范是一件很重要的事情。另外一点就是,不要局限于开源工具提供的能力,可以自己适当地去想办法优化或者改造,以达到自己的目的。
当然,文中所述流程不一定适合所有场景,仅供读者参考!
如果您对我的专栏感兴趣,欢迎您订阅关注本专栏,接下来可以一同探讨和交流组件库开发过程中遇到的问题。
The text was updated successfully, but these errors were encountered: