前端搞工程化:揭秘自动化部署系统(一)

12,559 阅读8分钟

发 npm 包对于稍微大点的厂来说都是个频繁需求,因此本地执行计算版本命令以及 npm publish 都是不大可行的,大都会有一个单独的部署系统去自动帮助我们完成这个事情。

今天就来聊聊这个部署系统中核心的计算版本以及发布的逻辑以及流程。

Semver 语义化版本

聊部署系统之前,我们先得来聊聊语义化版本,因为笔者发现很多人对于这一块内容还是一知半解。

版本含义

  • 小于 1.0.0:测试版,说明该库目前 API 不稳定
  • 大于等于 1.0.0:正式版
  • 版本中携带 alpha、beta、rc 等 tag 字样,统称先行版,一般格式为 x.y.z-[tag].[次数 / meta 信息]
    • alpha:内部版本
    • beta:公测版本
    • rc:预发的正式版本

版本号格式

一般的版本号格式都为 X.Y.Z,分别的含义为:

  • X:major,主版本号,当有不兼容的 API 出现时应该修改该版本号
  • Y:minor,次版本号,当有向后兼容的新功能出现时应该修改该版本号
  • Z:patch,补丁,当需要修复向后兼容的 bug 时应该修改该版本号

但是这个语义也不是一成不变的。比如当版本号为测试版时(版本小于 1.0.0 时),我们可以将语义修改为 0.minor.patch。因为此时出现不兼容 API 是很正常的事情,不应该直接改动主版本号,而是应该改动次版本号,同时将功能新增及 bug 修复造成的版本变更体现在补丁上。

版本变更规则

X.Y.Z 必须为正整数且前面不能补零。

X.Y.Z 在每次变更版本号时,需要重置更小的版本号至 0。比如说 1.0.2 升级至 1.1.0。

同一个版本的先行版多次发布,只需变更末尾的次数或者 meta 值。比如说 1.0.0-beta.0 再次发布先行版应为 1.0.0-beta.1。

测试版一般从 0.1.0 开始计算。正式版一般在结束快速迭代以及开发者认为 API 稳定以后就可以发布。

自动计算版本的前提条件

术语解释

npm 项目分为两种包结构:

  • 单包,一个项目中只存在一个需要发布的 npm 包
  • 多包,一个项目中存在多个需要发包的 npm 包,通常使用 lerna 管理

内容

如果我们需要实现自动发版,就得让服务知道我们到底是需要发什么版本,否则无论怎样都不可能实现自动计算版本的需求。

因此我们需要引入 commitizen 这个工具。

这个工具可以帮忙我们提交规范化的 commit 信息:

格式如下:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

一般对于日常开发来说没那么严谨,type、description 必填,breaking 在需要的时候选择,其他内容可有可无。

未免读者不明白这三者分别代表什么,笔者先来解释下

type 就是本次 commit 所做的代码变动,基本分为以下几种:

  • feat: 新特性,可以造成正式版变更次版本号
  • fix: 修改 bug,可以造成正式版变更补丁版本号
  • refactor: 代码重构,不会引起版本变更
  • docs: 文档相关,不会引起版本变更
  • style: 代码格式修改,不会引起版本变更
  • test: 测试用例,不会引起版本变更
  • chore: 工程配置相关的修改,不会引起版本变更

当然如果用户有个性化需求的话,也是可以增删这部分内容的。

description 就是字面意思了,代表本地 commit 的信息。

最后是 breaking,当有不兼容的 API 出现时我们需要提交这部分的内容,告知正式版需要变更主版本号。

PS:上文中说的都是正式版,如果当前版本为测试版的话,变更版本规则参考 版本变更规则小节

最后生成出来的内容大致长这个样子:fix: do not alter attributes

另外还有一个注意点是多包的问题。如果发版用统一版本的话事情就基本回归到单包结构上了,很简单。但是如果版本不统一的话,我们就需要收集到底有哪些包是变更过文件的,在部署时只对变更过的包进行操作。

这里大致有两种方法可以实现:

第一种方法是 @lerna/changed,这个工具可以帮助我们找到所有变更过的二方包。这里原理其实挺简单的,核心就是通过 git command 去实现:

git diff --name-only {git tag / commit sha} --{package path}

翻译过来就是寻找从上次的 git tag 或者初次的 commit 信息中查找某个包是否存在文件变更。

第二种方式可以改造 git cz 工具,新增一个功能:每次提交的时候自动带上本次提交变更了哪些包,当然底下还是用了上面的原理,但是我们可以根据需求来定制更多的功能,更自由。

规范化的 commit 信息是自动部署系统的基石,无论用工具也好还是直接手写,同时也能让开发者清晰地了解大致项目做了哪些变更。

部署系统中如何计算版本

计算版本是个挺有趣的东西,这里我们需要用到 semver 来帮助我们计算,当然多包场景下你也可以使用 lerna 计算,但是我们内部还是直接统一都 semver 算了。

另外算版本这件事情需要分场景来论,接下来我们一个个来看。

当然在开始之前,我们需要了解下版本的通用变更规则,因为几个场景都基于这个通用规则。

通用变更逻辑

首先来介绍下升版本所需要用到的几种类型:

major | minor | patch | premajor | preminor | prepatch | prerelease

前三种之前就聊过,这里不再多说。

之后三种都对应先行版,以 beta 版本举例 premajor,能将版本 1.0.0 变更为 2.0.0-beta.0,其实大体上还是和前三者相同,无非多了一个先行版本号。

最后种同样也是对应先行版。以 beta 版本举例,能将版本 1.0.0 变更为 1.0.1-beta.0,同时也能将版本 1.0.1-beta.0 变更为 1.0.1-beta.1。

知道了变更版本类型,我们就该想该如何得出它们了。一般来说用户都会提交多个 commit,我们首先需要找出其中所有的 commit type 并且取出一个最大值。

比如说用户自上次发版以来共提交了三个 commit,类型分别为 feat、doc、breakchange,那么最大类型为 breakchange。

得出最大 commit type 后,我们需要根据不同的版本来计算。比如说正式版与测试版发版规则就不同,详见 版本号格式,不再赘述。

举个例子,当前版本为 1.0.0,此时根据 commit 我们得出最大 type 为 feat,且需要发布先行版(beta),因此最终计算得出的变更规则为 preminor,可将版本升级为 1.1.0-beta.0。

截屏2021-03-16下午10.54.26

分析 commit 信息

上文有说到我们需要分析 commit 来获取 type,那么读者可能会疑问如何分析?

其实原理很简单,还是用到了 git command:

git log -E --format=%H=%B

对于以上 commit,我们可以通过执行命令得出以下结果:

当然这样分析是把当前分支的所有 commit 都分析进去了,大部分发版时候我们只需要分析上次发版至今的所有变更,因此需要修正 command 为:

git log 上次的 commit id...HEAD -E --format=%H=%B

最后就是各种正则表达式大显身手的时候了,想拿啥信息匹配就行。

单包场景

单包场景其实是最简单的,直接套用通用变更逻辑就行。

多包场景

单纯的多包环境其实也是简单的。无非比单包多了一步需要先找到哪些文件被变更了,然后需要筛选出 commit 中对应当前包的 type,最后就是套用通用变更逻辑。

多包且互有依赖场景

先解释下这个场景,比如说目前维护了 A、B、C 三个包,A 包的 dependencies 中包含了 B 和 C,这就是有依赖的意思,此时计算版本还需要多个步骤。

当通用逻辑结束以后,我们需要根据依赖关系来判断是否还需要变更包版本。

比如说本地提交需要执行 patch 变更 B 包版本,其它两包都没有代码变动。但是实际上我们还需要变动 A 的版本,否则光升 B 不升 A,用 A 包的用户就用不到 B 包的新版本变化了。

收尾

当我们将所有版本计算完毕以后,就需要写入 package.json 然后 执行 npm publish,最后还需要提交下代码打上 tag。

最后

本篇文章就是大致聊了下算版本以及发布这部分的内容,大家有问题的可以交流讨论。

另外我相信肯定有读者会说这做的太麻烦,有别的工具可以简化步骤。这个笔者当然也知道,但是他们底下自动修改版本的原理都是和本文一致的,了解下工具底下是怎么做事的也不为过。