语义化版本
在一探package-lock.json
究竟之前,你必须要理解semver。它是npm
背后的小小功臣。你可以从这里了解到npm
是如何使用它的。概括来讲,假若你在开发一个可供其它应用使用的应用(或者叫包依赖),你必须说明每次升级变更会对第三方使用产生哪些影响。这就是语义化版本想要传达的。一个版本有三部分:X, Y, Z,分别指代大版本,小版本,与查缺补漏版本。比如1.2.3,那么就是大版本1,小版本2,bugfix版本3。bugfix版本不会影响任何功能,小版本变更往往是增加新功能,也不会影响使用。而大版本变更往往会带来使用层面不兼容的情况,需要再做调整。(想想webpack每次升级的时候!)
NPM包管理
正是为了让包管理变简单,npm
出现了。一个项目可能有上百个依赖,每个依赖又有上百个依赖。为了你不陷入依赖地狱,只需简单几行命令,npm
就可以安装并管理这些依赖,大大节省了时间。
当你使用npm
安装一个包(并保存它)的时候,package.json
里就自动添加了一条信息,包括包名和其版本。npm
当然也支持版本的通配符。npm
默认安装最新版本,然后在其版本号之前添加一个^
符号。比如^1.2.12
,它表明最低应使用1.2.12
版本,并且在这之上,拥有相同大版本号的任何版本都是OK的。毕竟小版本和bugfix版本不会对使用造成任何影响,所以用任何相同大版本的更高级版本都很安全。可以从这里了解semver
通配符的更多信息,以及npm的semver计算器。
- 符号
^
:表示安装不低于该版本的应用,但是大版本号需相同,例如: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
版本。
为什么需要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_modules
,npm
使用package-lock.json
。无论何时运行npm install
,npm
都会生成或更新package-lock.json
。
不同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.json
的semver-range version
和package-lock.json
中版本兼容(package-lock.json
版本在package.json
指定的版本范围内),即使此时package.json
中有新的版本,执行npm i
也还是会根据package-lock.json
下载 ------ 示例参考下面实践场景一。 - 如果手动修改了
package.json
的version ranges
,且和package-lock.json
中版本不兼容,那么执行npm i
时package-lock.json
将会更新到兼容package.json
的版本 ------ 示例参考下面实践场景二。
- 如果只有一个
注意:
npm install
读取package.json
创建依赖项列表,并使用package-lock.json
来通知要安装这些依赖项的哪个版本。如果某个依赖项在package.json
中,但是不在package-lock.json
中,运行npm install
会将这个依赖项的确定版本更新到package-lock.json
中,不会更新其它依赖项的版本。
实践
场景一
// 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
版本。
场景二
// 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
。
关于npm源
最近开发中有一个新需求,需要使用 tim-js-sdk
,使用 npm install tim-js-sdk -S
下载 tim-js-sdk
后,发现 package-lock.json
中有一个包的 resolved
字段变成了 registry.npmjs.org
,具体说明如下 :
- 首先查询本地的 npm 源:
npm config get registry
- 然后下载
tim-js-sdk:npm install tim-js-sdk -S
;package-lock.json
会更新。package-lock.json
部分前后对比图:左边是更新前的,我同事开发时安装的,右边是更新后的,主要的变化就是resolved
字段从11x.2x.1xx.6x:4873
变成了registry.npmjs.org
。
node_modules
下@babel/generator
的package.json
部分截图:
node_modules
下@vant/weapp
的package.json
部分截图:
- 结论:
node_modules
下的每个包的package.json
中指定了_resolved
字段,当去更新package-lock.json
时,会去copy
这个字段,而不是当前npm
指定的源。很有可能是在初始化项目、一开始执行npm install
的时候的源是registry.npmjs.org
,后面即使你更新了源,也不会改变已经下载的包的_resolved
字段。可以通过删掉整个node_modules
重新下载解决这个问题,当然最好的方式是在项目根目录下创建一个.npmrc
的文件,在其中指定npm 源
,以保证团队的成员使用的是统一的npm 源
,这样就不会在拉取代码的时候出现package-lock.json
中有很多冲突的情况。
关于npm ci
介绍
- ci:Continuous 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 ci 和 npm install 差异
- 项目必须存在 package-lock.json 或 npm-shrinkwrap.json。
- 如果 package-lock.json 中的依赖和 package.json 中不匹配,npm ci 会退出并且报错,而不是去更新 package-lock.json。
- npm ci 只能安装整个项目的依赖,无法安装单个依赖。
- 如果 node_modules 已经存在,它将在 npm ci 开始安装之前自动删除。
npm ci 永远不会改变 package.json 和 package-lock.json。 - 缓存
npm ci --cache .npm
- npm ci 时建议加上
--quiet --no-progress
关闭进度和其他无用 log,否则产生的日志会很大。 - 所以 ci 时推荐完整的命令为
npm ci --cache .npm --quiet --no-progress
npm和cnpm的差异
- cnpm i不受package-lock.json影响,只会根据package.json进行下载。
- cnpm i xxx@xxx不会跟新到package-lock.json中去。
- npm i xxx@xxx会跟新到package-lock.json中去。
- 总结:所以在实际开发中不建议使用cnpm