npm依赖包的版本锁定原理

4,524 阅读4分钟

前言

在软件管理的领域里存在着被称作依赖地狱的死亡之谷,系统规模越大,加入的包越多,就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

因此,Github 起草了一个具有指导意义的、统一的版本号表示规则,称为 Semantic Versioning(语义化版本),即 Semver。目前它是由 npm 的团队维护。

语义化版本(semver)

语义化版本格式:X.Y.Z(主版本号.次版本号.修订号)

版本号递增规则如下:

  • 主版本号:做了不兼容的 API 修改(进行不向下兼容的修改)
  • 次版本号:做了向下兼容的功能性增加(API 保持向下兼容的新增及修改)
  • 修订号:做了向下兼容的问题修正(修复问题但不影响 API)

先行版本号及版本编译信息可以加到  “主版本号.次版本号.修订号”  的后面,作为延伸。比如:1.0.0-alpha.0,1.0.0-alpha.1,1.0.0-beta.0,1.0.0-rc.0,1.0.p-rc.1 等版本。

关于 Semver 的完整规则可以查阅 Semver官网

依赖版本号规则

npm依赖规则中,还有>>=<<=~^x*-||等符号。通过在版本号前面加上这些符号,可以有效的限制依赖的版本。

  1. version:必须依赖某个具体的版本。如:2.5.2,表示必须安装2.5.2版本。
  2. >version:必须大于某个版本。
  3. >=version:大于或等于某个版本。
  4. <version:必须小于某个版本。
  5. <=version:小于或等于某个版本
  6. ~version:大概匹配某个版本。
    如果次版本号(Y)指定了,那么次版本号(Y)不变,而修订号(Z)任意。
    如果次版本号(Y)和修订号(Z)未指定,那么次版本号(Y)和修订号(Z)任意。
"~2.5.2" // 表示 >=2.5.2 且 <2.6.0,可以是2.5.3,2.5.4,2.5.5,..., 2.5.n
"~2.5" // 表示 >=2.5.0 且 <2.6.0
"~2" // 表示 >=2.0.0 且 <3.0.0
  1. ^version:向上兼容某个版本。从版本号最左侧开始,到首个非零数字,这些位置固定,其余位置任意。如果缺少某个位置,则这个位置可以任意。
"^2.5.2" // 表示 >=2.5.2 且 <3.0.0
"^0.1.3" // 表示 >=0.1.3 且 <0.2.0
"^0.0" // 表示 >=0.0.0 且 <0.1.0
  1. x-range:x的位置可以为任意版本。
"2.x" // 表示可以安装主版本号为2的任意版本。
"2.5.x" // 表示可以安装2.5.0,2.5.1,2.5.2, ...  2.5.n等 版本
  1. *-range:任意版本。“”也表示任意版本。
  2. version1 - version2:大于等于version1,小于等于version2。
  3. range1 || range2:满足range1或者满足range2,可以有多个范围。

锁定版本号

默认情况下, npm install --save下载的都是最新版本,并且会在package.json 文件中登记一个最优版本号,即版本号前会默认加上 ^ 符号。如:

{
  "dependencies": {
    "vue": "^3.2.41"
  }
}

如果所有的node包都严格符合语义化版本(semver)管理的规则,那么npm的最优版本号就能保证所下载的依赖包一定是与代码兼容的。由于无法保证这一前提,如果想要保证用户(或者其他开发人员)下载依赖包与我们的代码绝对兼容,就需要锁定项目中依赖包的版本号。

回避最优版本号

最简单的方法,就是指定具体版本号,可以将package.json中版本号开头的^~等标记去掉。后续安装新的依赖包时,则使用npm install --save-exact <package_name>或者npm install --save <package_name>@1.2.3(指定依赖的具体版本),这样package.json中就不会出现最优版本的标记。

缺陷:无法锁定次级依赖的版本号,即依赖包的依赖包。

npm shrinkwrap && package-lock.json

由于在重新安装依赖时,依赖树模块的版本存在着不确定性,为了解决这个问题,npm提供了npm-shrinkwrap.jsonpackage-lock.json文件,这两种文件被称为包锁或锁文件。

npm shrinkwrap

在npm 5版本以前(不包括npm 5),可以通过执行npm shrinkwrap命令,创建一个新的或覆盖已有的 npm-shrinkwrap.json 文件。该文件记录了目前所有依赖包(及更底层依赖包)的版本信息。当再次运行npm install命令重新安装依赖时,npm首先会找npm-shrinkwrap.json文件,依照其中的信息来准确地安装每一个依赖包,只有当这个文件不存在时,npm才会使用package.json

注意: 每次更新package.json或者node_modules时,如:npm install新包、npm updatenpm uninstall等操作,为了保证所有开发人员的资源一致,还是要手动运行npm shrinkwrap更新npm-shrinkwrap.json文件。npm shrinkwrap计算时是根据当前依赖安装的目录结构生成的,如果不能保证package.json文件定义的依赖与node_modules下已安装的依赖是匹配、无冗余的,建议在执行npm shrinkwrap命令前清理依赖并重新安装(rm-rf node_modules&&npm install)或精简依赖(npm prune)。

package-lock.json

在npm 5以后,运行npm intall会自动生成一个新文件package-lock.json,其内容跟上面提到的npm-shrinkwrap.json基本一样,在修改pacakge.json或者node_modules时会自动产生或更新它。

当项目中已存在package-lock.json文件,再安装项目依赖时,将以该文件为主进行解析安装指定版本的依赖包,而不是使用package.json来解析和安装。因为package-lock.json为每个模块及其每个依赖项都指定了版本、位置和完整性哈希,所以它每次创建的安装都是相同的。

注意:cnpm并不支持package-lock。 使用cnpm install时,并不会生成package-lock.json文件。即使项目中已有package-lock.json文件,执行cnpm install命令,cnpm 也不会识别,仍会根据package.json安装依赖。因此,尽量避免直接使用cnpm install安装项目的依赖。

区别和联系

  1. package-lock.json是npm 5的新特性,且不向下兼容,因此如果npm版本是5以下,还是使用npm shrinkwrap命令。
  2. package-lock.jsonnpm-shrinkwrap.json这两个文件的优先级都比package.json高。同一个项目里,如果不存在这两个文件,在运行npm install或者初始化项目npm init时,会自动生成一个package-lock.json(npm 5及以上)。如果这两个文件都存在,安装依赖则是依据npm-shrinkwrap.json,而忽略package-lock.json。如果项目里不存在package-lock.json,运行命令npm shrinkwrap后,会创建一个npm-shrinkwrap.json文件,如果存在package-lock.json,则会将其重命名为npm-shrinkwrap.json
  3. npm-shrinkwrap.json只有在运行npm shrinkwrap命令时才会创建或更新;而package-lock.json会在修改pacakge.json或者node_modules时自动产生或更新。

结语

本文主要介绍了语义化版本、版本号规则、版本锁定等相关内容,希望可以帮助到你。