如何管理 npm 版本号:语义化版本策略 SemVer

语义化版本(SemVer)

什么是 SemVer

由于软件开发中会依赖各式各样的依赖库(包),随着数量的增多,以及版本的不断迭代,项目管理者必然面对这样的问题:如何正确的管理这些包的版本?

那通过什么来约定呢?所以就有了 SemVer:

SemVer(Semantic Versioning),语义化版本控制。

SemVer 通过不同的语法规则来约定不同版本间的升级方案,最终使得我们可以“随心所欲”的更新版本。

比如,前端项目 package.json 中包版本定义就是基于 SemVer 规范(npm 中实现了它):

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"less": "^4.1.0",
"less-loader": "^7.3.0",
"vue-template-compiler": "^2.6.11"
}
}

下面就来看看这些语义化规范和语法规则。

语义化规范

为了下面能更好的理解规则,首先了解下相关语义化的含义:

版本号格式

1
X.Y.Z; //主版本号.次版本号.修订号

标准的版本号必须为 X.Y.Z 的格式,且都为非负的整数。禁止数字前面补零(比如:02)

X 是主版本号(major version)、Y 是次版本号(minor version)、而 Z 为修订号(patch version)

版本只能每次 +1 的往上自然增加(比如:1.9.0 -> 1.10.0 -> 1.11.0)

另外,还有一些先行版版本号:

1
X.Y.Z-alpha.1

约定先行版本号是在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。

标识符必须由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成。比如:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

更新 X、Y、Z 的场景

  1. 修订号 Z(patch version)

    必须做向下兼容的修复时才递增(例如:修复 bug)

  2. 次版本号 Y(minor version)

    必须向下兼容的新功能出现时递增,当某些功能被弃用也必须递增;

    另外,当有大量新功能和改动时,也可以递增此版本号。

    注:每次递增,修订号必须归零。

  3. 主版本号 X(major version)

    必须在有任何不兼容的修改被加入公共 API 时递增。

    注:每次递增,次版本号和修订号必须归零。

规则说明

一般规则

  • <2.0.0 小于某版本
  • <=2.0.0 小于 or 等于某版本
  • >2.0.0 大于 or 等于某版本
  • >=2.0.0 大于 or 等于某版本
  • =2.0.0 精确等于某版本

上面这些规则都很好理解,简单看下下面例子:

1
2
3
4
5
6
{
"dependencies": {
"moment": "2.20.0",
"axios": "<0.19.4"
}
}

那么执行 npm install 后,我们将准确得到版本为 2.20.0moment 包,
以及最接近 0.19.4 版本为 0.19.2 axios

你有可能会问为什么不是 0.19.3

我们可以通过 npm view 来查询有关 axios 所有的版本:

1
npm view axios versions
1
2
3
4
5
6
7
8
9
10
11
12
13
[
//...
'0.18.0',
'0.18.1',
'0.19.0-beta.1',
'0.19.0',
'0.19.1',
'0.19.2',
'0.20.0-0',
'0.20.0',
'0.21.0',
'0.21.1',
];

能看到压根没有 0.19.3,直接跳到了 0.20.0-0

高级规则

实际开发中,我们更多的是见到以下这些规则:

破折号策略(-)

1
1.2.3 - 2.3.4 // 代表 >=1.2.3 <=2.3.4 之间的版本,包含左右版本。

如果起始版本(左侧的版本)有空缺,将以 0 补位:

1
1.2 - 2.3.4 // 代表 >=1.2.0 <=2.3.4

如果结尾版本(右侧的版本)有空缺,将以 0 补位,并且递增非 0 版本号作为最大版本号:

1
2
3
1.2.3 - 2.3 // >=1.2.3 <2.4.0

1.2.3 - 2 // >=1.2.3 < 3.0.0

泛版本策略(*)

可以使用 X, x, or * 来作为某个版本号的占位符,来示意所有可能的版本号。

1
2
3
4
5
* // 代表 >=0.0.0 (所有版本)

1.x // 代表 >=1.0.0 <2.0.0 (主版本限定为 1 的版本号)

1.2.x // 代表 >=1.2.0 <1.3.0 (主版本+次版本限定为 1.2 的版本号)

如果我们版本号有缺损,将为我们自动以占位符填充:

1
2
3
4
5
"" (empty string) // 代表 * 即 >=0.0.0

1 // 代表 1.x.x 即 >=1.0.0 <2.0.0

1.2 // 代表 1.2.x 即 >=1.2.0 <1.3.0

波浪策略(~)

当前版本号为起始版本,以倒数第二个版本号+1(次版本号 Y)为递增版本,可更新 [起始,结束) 范围内的所有版本号。

如有版本号有空缺,将自动补 0,并以最近的空缺版本之前的版本号+1 作为递增的最大版本号。

对于 0.Y.Z 的开发阶段的版本号,将递增 0 后面的版本号作为递增的最大版本号。

1
2
3
4
5
6
7
8
9
10
11
12
13
~1.2.3 // 代表 >=1.2.3 <1.(2+1).0 即 >=1.2.3 <1.3.0

~1.2 // 代表 >=1.2.0 <1.(2+1).0 即 >=1.2.0 <1.3.0 (等同 1.2.x)

~1 // 代表 >=1.0.0 <(1+1).0.0 即 >=1.0.0 <2.0.0 (等同 1.x)

~0.2.3 // 代表 >=0.2.3 <0.(2+1).0 即 >=0.2.3 <0.3.0

~0.2 // 代表 >=0.2.0 <0.(2+1).0 即 >=0.2.0 <0.3.0 (等同 0.2.x)

~0 // 代表 >=0.0.0 <(0+1).0.0 即 >=0.0.0 <1.0.0 (等同 0.x)

~1.2.3-beta.2 // 代表 >=1.2.3-beta.2 <1.3.0

倒三角策略(^)

更新主版本号,如果版本号为 0,则往下取非 0 版本号递增,作为最大版本号。

1
2
3
4
5
^1.2.3 // 代表 >=1.2.3 <2.0.0
^0.2.3 // 代表 >=0.2.3 <0.3.0
^0.0.3 // 代表 >=0.0.3 <0.0.4
^1.2.3-beta.2 // 代表 >=1.2.3-beta.2 <2.0.0
^0.0.3-beta // 代表 >=0.0.3-beta <0.0.4

对于泛版本,以及空缺版本的示例:

1
2
3
^1.2.x // 代表 >=1.2.0 <2.0.0
^0.0.x // 代表 >=0.0.0 <0.1.0
^0.0 // 代表 >=0.0.0 <0.1.0
1
2
^1.x // 代表 >=1.0.0 <2.0.0
^0.x // 代表 >=0.0.0 <1.0.0

最后

这篇只是简单说下 package.json 中版本号的更新规则,或许你每次 install 后从不关心版本升级策略,但随着项目功能迭代的增多,总会遇到团队某个开发人员安装了某个包后用了新特性,但在你的电脑上频繁报错。

可能你会在依赖包中“锁定”版本,但我们不能因噎废食,尽可能依靠灵活的版本升级策略,依靠社区为我们项目提供“生命力”。

参考

【长按关注】看看↓↓↓?
Eminoda wechat
【前端雨爸】分享前端技术实践,持续输出前端技术文章
欢迎留言,评论交流,一起讨论前端问题
📢 因为是开源博客,为避免 Gitalk授权 带来的 安全风险,也可访问