由一个构建问题深入思考SemVer

起因

一个阳光明媚的早上,小A同学匆匆赶到公司,打开ta心爱的MacBook Pro M1(16英寸),git checkout -b xx git push origin xxx:xxx一气呵成,今天ta要赶在中午12:00封板前完成最后一次 commit。

编码、调试、提交代码、运行流水线发布测试环境,一切似乎很顺利,然而一个流水线运行错误告警让ta头皮一阵发麻。

背景知识:该项目技术栈为 React18 + TDesign + React-redux

这个错误提示表明node.children的类型有可能是boolean,而boolean没有length属性,因此这里的判断被typescript判定为异常

奇怪了,上个月都还好好的,这段代码最近一个月也没有任何修改,怎么今天会突然报错呢?

one hour later...真相原来如此。

查看本地node_modules里的代码

发现children属性的类型声明确实为Array:

构建机安装的node_modules代码

发现children属性的类型为Arrayboolean的联合类型!!!:

为什么会突然变了呢?直觉告诉我得去看下构建机安装的TDesign的真实版本:

安装的是1.3.0版本

真相大白,原来是版本自动升级导致,并且从错误触发时间可以推测,最近TDesign应该有发版本,并且就是在最近的版本里修改了children属性的类型声明(tdesign包发版记录):

为什么流水线突然安装1.3.0版本呢?

有这个疑问是因为在项目的packag.json里对tdesign包的依赖声明为:
"tdesign-react": "^1.0.5"

为此,特意复习了一下SemVer版本的知识...

什么是 SemVer

由于软件开发中会依赖各式各样的依赖库(包),随着数量的增多,以及版本的不断迭代,开发者必然面对这样的问题:如何正确的管理这些包的版本? SemVer(Semantic Versioning),语义化版本控制通过不同的语法规则来约定不同版本间的升级方案,最终使得开发者可以"随心所欲"的正确更新版本。目前,前端技术领域最广泛使用的npm仓库中的所有代码库均遵循了SemVer规范。

规范内容

版本号格式

arduino 复制代码
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),不允许跳版本号增加,更不允许递减。 另外,还允许有先行版版本号:

matlab 复制代码
X.Y.Z-beta.1

并约定:先行版本号是在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。其中,标识符必须由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成。比如:1.0.0-、1.0.0-beta.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

更新版本号的场景

修订号 Z(patch version)

必须做向下兼容的修复时才递增,一般用在修复bug的场景。

次版本号 Y(minor version)

必须向下兼容的新功能出现时递增,当某些功能被弃用也必须递增; 另外,当有大量新功能和改动时,也可以递增此版本号。

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

主版本号 X(major version)

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

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

结论

当需要升级项目的依赖包的主版本时往往意味着对旧有代码的不兼容,需要开发团队全体成员商讨决定并制定兼容方案。 当升级涉及到次版本号时则只需关注那些被废弃的功能或特性,当然也可以不关注,因为一般来讲作者都会将保留废弃方法,只是在官方文档里不建议继续使用废弃的方法或特性 而如果升级只涉及修订号,那则完全不用担心兼容性问题。

规则说明

一般规则

shell 复制代码
<2.0.0 小于2.0.0的最新版本
<=2.0.0 小于 or 等于2.0.0的最新
>2.0.0 大于2.0.0的最新版本
>=2.0.0 大于 or 等于2.0.0最新版本
=2.0.0 精确等于2.0.0版本

来看一个例子

json 复制代码
{
  "dependencies": {
    "moment": "2.20.0",
    "axios": "<0.19.4"
  }
}

那么执行 npm i 后,我们将准确得到版本为 2.20.0 的 moment 包,以及最接近 0.19.4 版本为 0.19.2 axios 包。 你有可能会问为什么不是 0.19.3? 我们可以通过 npm view 来查询有关 axios 所有的版本:

sql 复制代码
npm view axios versions
css 复制代码
[  '0.1.0',    '0.2.0',  '0.2.1',  '0.2.2',    '0.3.0',  '0.3.1',  '0.4.0',    '0.4.1',  '0.4.2',  '0.5.0',    '0.5.1',  '0.5.2',  '0.5.3',    '0.5.4',  '0.6.0',  '0.7.0',    '0.8.0',  '0.8.1',  '0.9.0',    '0.9.1',  '0.10.0',  '0.11.0',   '0.11.1', '0.12.0',  '0.13.0',   '0.13.1', '0.14.0',  '0.15.0',   '0.15.1', '0.15.2',  '0.15.3',   '0.16.0', '0.16.1',  '0.16.2',   '0.17.0', '0.17.1',  '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。所以低于0.19.4且离它最近的版本是0.19.2。

高级规则

破折号策略(-)

1.2.3 - 2.3.4 // 代表 >=1.2.3 <=2.3.4 之间的版本,包含左右版本。 如果起始版本(左侧的版本)有空缺,将以 0 补位: 1.2 - 2.3.4 // 代表 >=1.2.0 <=2.3.4 如果结尾版本(右侧的版本)有空缺,将以 0 补位,并且递增非 0 版本号作为最大版本号: 1.2.3 - 2.3 // 代表>=1.2.3 <2.4.0 1.2.3 - 2 // 代表>=1.2.3 < 3.0.0

泛版本策略(*)

可以使用 X, x, or * 来作为某个版本号的占位符,来示意所有可能的版本号。 * // 代表 >=0.0.0 (所有版本) 1.x // 代表 >=1.0.0 <2.0.0 (主版本限定为 1 的版本号) 1.2.x // 代表 >=1.2.0 <1.3.0 (主版本+次版本限定为 1.2 的版本号) 如果我们版本号有缺损,将为我们自动以占位符填充: "" (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

波浪策略(~)

波浪号后的版本为下限版本;然后保持主版本号X不变,次版本号Y递增1的版本为上限版本。 如X.Y.Z格式的版本号有空缺(比如:1,1.2),将自动补 0(补为 1.0.0,1.2.0),并以最近的空缺版本之前的版本位+1 作为上限版本。比如1,补全后为1.0.0,空缺版本是Y,Z。Y之前的版本位是X,因此(X+1).Y.Z才是上限版本。故~1表示 >=1.0.0 <2.0.0。 更多示例:

arduino 复制代码
~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

倒三角策略(^)

主版本号递增1为上限版本,以当前版本为下限版本。 另外,如果最左侧版本号为0,则从左往右找到非零版本号递增1,作为上限版本。 也就是说如果主版本号为0、次版本号不为0,就以次版本号增1时的版本作为上限版本。次版本号也为0时就递增修订号作为上限版本。

arduino 复制代码
^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

泛版本结合波浪策略、倒三角策略

对于泛版本结合波浪策略、倒三角策略,以及空缺版本的更多示例:

arduino 复制代码
^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.x // 代表 >=1.0.0 <2.0.0
^0.x // 代表 >=0.0.0 <1.0.0

最终答案

从上面的叙述可知,根据倒三角策略,package.json里声明的"tdesign-react": "^1.0.5"合法的可安装版本范围是:>= 1.0.5 & < 2.0.0

流水线运行时构建机上安装1.3.0版本确实在合法范围内。

  • Question1:不是有pnpm-lock.yaml文件吗?CI的时候怎么不能锁定版本呢?

    Answer:pnpm-lock.yaml的作用机制跟package-lock.jsonyarn.lock类似,其锁定版本是找到当前最合适的兼容版本进行安装,也就是说只要安装的版本兼容了lock文件里的版本,pnpm|npm|yarn是会出现"自动"升级版本的情况的。

  • Question2:为什么本地开发时没有出现,但是在CI阶段出现了呢?

    Answer:因为本地开发时node_modules没有更新,项目没有增加新的依赖,执行安装时默认用了之前安装的包。

参考

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试