浅谈npm 构建流程及package.json和 package.lock.json的工作流程

一、起因

在一次日常代码检视中,发现库上居然上了package-lock.json。这是静态记录版本所有依赖以及子依赖的文件。由于这个文件的版本第一次提交到目前9个月的时间都没有进行更新,在这期间,前端库有大大小小的package.json依赖库的升级,但是package-lock.json 这个文件却没有升级。于是我一个MR,删除了库上文件,本来是改动的开发版本。但是由于开发版本与构建版本在一台机器上,npm clean cache指令清除了机器上所有的缓存包,因此同时影响了发布版本的构建。造成库上node_modules中的子依赖项升级,导致部分兼容性问题。由于是版本发布前夕发现的问题,故影响较大。

二、分析

package.lock.json文件到底是什么文件?他的作用是什么?到底该不该上库?接下来就镇定思痛,赋能一波npm install过程,以及package.lock.json的作用。

1、语义化版本

在一探package-lock.json究竟之前,你必须要理解semver。它是npm背后的小小功臣。你可以从这里了解到npm是如何使用它的。概括来讲,假若你在开发一个可供其它应用使用的应用,你必须说明每次升级变更会对第三方使用产生哪些影响。这就是语义化版本想要传达的。一个版本有三部分:X, Y, Z,分别指代大版本,小版本,与查缺补漏版本。比如1.2.3,那么就是大版本1,小版本2bugfix版本3bugfix版本不会影响任何功能,小版本变更往往是增加新功能,也不会影响使用。而大版本变更往往会带来使用层面不兼容的情况,需要再做调整。(想想webpack每次升级的时候!)

2、npm 包管理

正是为了让包管理变简单,npm出现了。一个项目可能有上百个依赖,每个依赖又有上百个依赖。为了你不陷入依赖地狱,只需简单几行命令,npm就可以安装并管理这些依赖,大大节省了时间。

当你使用npm安装一个包(并保存它)的时候,package.json里就自动添加了一条信息,包括包名和其版本。npm当然也支持版本的通配符。npm默认安装最新版本,然后在其版本号之前添加一个^符号。比如 ^1.2.12,它表明最低应使用1.2.12版本,并且在这之上,拥有相同大版本号的任何版本都是OK的。毕竟小版本和bugfix版本不会对使用造成任何影响,所以用任何相同大版本的更高级版本都很安全。可以从这里了解semver通配符的更多信息,以及npmsemver计算器。

  • 符号^ :表示安装不低于该版本的应用,但是大版本号需相同,例如:vuex: "^3.1.3"3.1.3及其以上的3.x.x都是满足的。
  • 符号~:表示安装不低于该版本的应用,但是大版本号和小版本号需相同,例如:vuex: "^3.1.3"3.1.3及其以上的3.1.x都是满足的。
  • 无符号:无符号表示固定版本号,例如:vuex: "3.1.3",此时一定是安装3.1.3版本。

3、为什么需要package-lock.json

npm install的输入是package.json,它的输出是一棵node_modules树。理想情况下,npm install应该像纯函数一样工作,对于同一个package.json总是生成完全相同的node_modules树。在某些情况下,确实如此。但在其他很多情况中,npm无法做到这一点。有以下原因:

  • 不同版本的npm的安装算法不同。
  • 某些依赖项自上次安装以来,可能已发布了新版本,因此将根据package.json中的semver-range version更新依赖。
  • 某个依赖项的依赖项可能已发布新版本,即使您使用了固定依赖项说明符(是"1.2.3"而不是"^1.2.3"),它也会更新,因为你无法固定子依赖项的版本。
// package.json
"dependencies": {
    
    
    "packageA": "^2.1.0"
}

而依赖项版本更新可能会带来一些问题,例如:A新建了一个项目,生成了上面这份package.json文件,但A安装依赖的时间比较早,此时packageA的最新版本是2.1.0,该版本与代码兼容,没有出现bug。后来B克隆了A的项目,在安装依赖时packageA的最新版本是2.2.0,那么根据语义npm会去安装2.2.0的版本,但2.2.0版本的API可能发生了改动,导致代码出现bug。

这就是package.json会带来的问题,同一份package.json在不同的时间和环境下安装会产生不同的结果。

理论上这个问题是不应该出现的,因为npm作为开源世界的一部分,也遵循一个发布原则:相同大版本号下的新版本应该兼容旧版本。即2.1.0升级到2.2.0时API不应该发生变化。

但很多开源库的开发者并没有严格遵守这个发布原则,导致了上面的这个问题。

为了在不同的环境下生成相同的node_modulesnpm使用package-lock.json。无论何时运行npm installnpm都会生成或更新package-lock.json

4、不同npm版本下npm i的规则

  • npm 5.0.x版本:不管package.json中依赖是否有更新,npm i都会根据package-lock.json下载。针对这种安装策略,有人提出了这个issue - #16866 ,然后就演变成了5.1.0版本后的规则。
  • 5.1.0版本后:当package.json中的依赖项有新版本时,npm install会无视package-lock.json去下载新版本的依赖项并且更新package-lock.json。针对这种安装策略,又有人提出了一个issue - #17979,参考 npm 贡献者 iarna 的评论,得出5.4.2版本后的规则。
  • 5.4.2版本后
    • 如果只有一个package.json文件,运行npm i会根据它生成一个package-lock.json文件,这个文件相当于本次install的一个快照,它不仅记录了package.json指明的直接依赖的版本,也记录了间接依赖的版本。
    • 如果package.jsonsemver-range versionpackage-lock.json中版本兼容(package-lock.json版本在package.json指定的版本范围内),即使此时package.json中有新的版本,执行npm i也还是会根据package-lock.json下载 - 实践场景1。
    • 如果手动修改了package.jsonversion ranges,且和package-lock.json中版本不兼容,那么执行npm ipackage-lock.json将会更新到兼容package.json的版本 - 实践场景2。

注意:npm install读取package.json创建依赖项列表,并使用package-lock.json来通知要安装这些依赖项的哪个版本。如果某个依赖项在package.json中,但是不在package-lock.json中,运行npm install会将这个依赖项的确定版本更新到package-lock.json中,不会更新其它依赖项的版本。

5、实践

场景1
// package.json
"dependencies": {
    
    
	"vue": "^2.0.0"
}

// package-lock.json
"dependencies": {
    
    
	"vue": {
    
    
		"version": "2.1.0",
		"resolved": "https://registry.npm.taobao.org/vue/download/vue-2.1.0.tgz",
		"integrity": "sha1-KTuj76rKhGqmvL+sRc+FJMxZfj0="
	}
}

这种情况下package-lock.json指定的2.1.0^2.0.0指定的范围内,npm install会安装vue2.1.0版本。

场景2
// package.json
"dependencies": {
    
    
	"vue": "^2.2.0"
}

// package-lock.json
"dependencies": {
    
    
	"vue": {
    
    
		"version": "2.1.0",
		"resolved": "https://registry.npm.taobao.org/vue/download/vue-2.1.0.tgz",
		"integrity": "sha1-KTuj76rKhGqmvL+sRc+FJMxZfj0="
	}
}

这种情况下package-lock.json指定的2.1.0不在 ^ 2.2.0指定的范围内,npm install会按照^2.2.0的规则去安装最新的2.6.10版本,并且将package-lock.json的版本更新为2.6.10

三、引用本次例子

这次删除package.lock.json之后,本质原因是因为库上的antd的子依赖项rc-pagination升级导致。虽然咱们引用antd的版本没有变化,都是3.21.3。但是在子依赖描述中,antd是这样的:

在这里插入图片描述

对照semver的规则,这里是要求依赖的这个组件的大版本和小版本保持一致,对于修订版本不作要求。还原之前库上代码删除的package-lock.json,发现对于rc-pagination依赖,下载的是1.20.12版本,而删除package-lock.json后,新的install生成的package-lock.json锁定的版本是1.20.15

两个package.lock.json文件对比

然而,对于rc-pagination的版本升级,开发者没有遵循上述的semver规则,其中某个修订版本,有了不兼容修改,对于分页组件,pageSize必须默认传值,而不是由计算得出。故产品中所有依赖分页组件的模块,如果没有给pageSize传递默认值,通通报错白屏

四、镇定思痛

在这里,需要引出npm ci的概念:

关于npm ci
  • ciContinuous Integration,持续集成。
  • npm 版本至少是 v5.7.1
  • 此命令与 npm install 类似,不同之处在于它旨在用于自动化环境,例如集成测试环境、线上环境、或者您希望确保干净安装依赖项的任何情况。通过跳过某些面向用户的功能,它可以比常规的 npm install快得多。它也比常规安装更严格,它可以捕获由于本地环境的增量安装引起的错误或不一致。
  • npm ci是根据 package-lock.json去安装确定的依赖,package.json 只是用来验证是不是有不匹配的版本,假设package-lock.json 中存在一个确定版本的依赖 A,如果 package.json 中不存在依赖A 或者依赖 A 版本和lock中不兼容,npm ci就会报错。
npm cinpm install 差异
  • 项目必须存在 package-lock.jsonnpm-shrinkwrap.json
  • 如果 package-lock.json 中的依赖和package.json 中不匹配,npm ci会退出并且报错,而不是去更新 package-lock.json
  • npm ci只能安装整个项目的依赖,无法安装单个依赖。
  • 如果node_modules已经存在,它将在npm ci开始安装之前自动删除。
  • npm ci 永远不会改变package.jsonpackage-lock.json

我们目前的CI构建流程脚本中使用的一直是npm i。即使package.lock.json文件上库,对于大版本的依赖变动,我们安装的包也是不确定的。而且 npm i指令需要耗费大量的时间进行依赖检查,远程地址查询,本地依赖对比等操作,比较费时。对于这两点,我们完全可以通过package-lock.json 上库 + npm ci指令,控制库上每次自动化构建流程下载的依赖包固定不变。对于package.jsonpackage-lock.json文件的更新,由团队内统一的一个负责人负责,每次大版本变更,确定一个依赖的子依赖版本树,这样保证其他人不会改动到package-lock.json,且团队内部每个人下载的包固定不变。

猜你喜欢

转载自blog.csdn.net/qq_29722281/article/details/108550752