npm lockfiles
为什么需要 lockfiles?
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),它也会更新。
为了在不同的环境下生成相同的 node_modules,npm 使用 package-lock.json 或 npm-shrinkwrap.json。这两个文件都被称为 lockfiles。无论何时运行 npm install,npm 都会生成或更新 lockfiles。以下只讨论其中的 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 文件。 -
如果 package.json 的 semver-range version 和 package-lock.json 中版本兼容,即使此时 package.json 中有新的版本,执行
npm i
也还是会根据 package-lock.json 下载 - 实践场景1。 -
如果手动修改了 package.json 的 version ranges,且和 package-lock.json 中版本不兼容,那么执行
npm i
时 package-lock.json 将会更新到兼容 package.json 的版本 - 实践场景2。
-
实践
npm 版本:6.4.1
场景1
-
假设刚从远程仓库克隆一个项目,此时本地 node_modules 还不存在,package.json 和 package-lock.json 如下。已知 superagent 3.x.x 的最新版本是 3.8.3,那么运行 npm install 是根据 package-lock.json 中指定的版本 3.5.1 去下载还是根据 package.json 去下载最新的 3.x.x ?
// package.json "dependencies": { "superagent": "^3.5.1" } // package-lock.json { "superagent": { "version": "3.5.1", "resolved": "https://npm.garenanow.com/superagent/-/superagent-3.5.1.tgz", "integrity": "sha1-Ck+u/aM2d3d4iDR917TSH0EMhxs=", "requires": { "component-emitter": "^1.2.0", "cookiejar": "^2.0.6", "debug": "^2.2.0", "extend": "^3.0.0", "form-data": "^2.1.1", "formidable": "^1.1.1", "methods": "^1.1.1", "mime": "^1.3.4", "qs": "^6.1.0", "readable-stream": "^2.0.5" } }, }
结论:下载的是
3.5.1
。此时 package.json 和 package-lock.json 同时存在,package.json 的 version 是^3.5.1
,package-lock.json 的 version 是3.5.1
,并且当前 node_modules 中下载的也是3.5.1
。
场景2
- 接着场景1,然后手动修改 package.json 中 superagent 的版本为
^5.1.0
,再执行 npm i,发现不管有没有删除已有的 node_modules,package-lock.json 中 superagent 的版本都变成了5.1.0
,node_modules 中的也变成了5.1.0
。
参考
npm ci
介绍
- ci:Continuous Integration。
- npm 版本至少是 v5.7.1。
- 此命令与 npm install 类似,不同之处在于它旨在用于自动化环境,例如集成测试环境、线上环境、或者您希望确保干净安装依赖项的任何情况。通过跳过某些面向用户的功能,它可以比常规的 npm install 快得多。它也比常规安装更严格,它可以捕获由于本地环境的增量安装引起的错误或不一致。
npm ci 和 npm install 差异
- 项目必须存在 package-lock.json 或 npm-shrinkwrap.json。
- 如果 lockfiles 中的依赖和 package.json 中不匹配,npm ci 会退出并且报错,而不是去更新 lockfiles。
- npm ci 只能安装整个项目的依赖,无法安装单个依赖。
- 如果 node_modules 已经存在,它将在 npm ci 开始安装之前自动删除。
- npm ci 永远不会改变 package.json 和 package-lock.json。
补充
-
npm install 读取 package.json 创建依赖项列表,并使用 package-lock.json 来通知要安装这些依赖项的哪个版本。如果某个依赖项在 package.json 中,但是不在 package-lock.json 中,运行 npm install 会将这个依赖项的确定版本更新到 package-lock.json 中。
-
npm ci 是根据 package-lock.json 去安装确定的依赖,package.json 只是用来验证是不是有不匹配的版本,假设 package-lock.json 中存在一个确定版本的依赖 A,如果 package.json 中不存在依赖 A 或者依赖 A 版本和 lock 中不兼容,npm ci 就会报错。
参考
补充
最近在开发一个新需要,需要使用 echarts,使用 npm install echarts -S 下载 echarts 后,发现 package-lock.json 中很多其他包的 resolved 字段变成了 registry.npmjs.org,具体说明如下 :
-
首先查询本地的 npm 源:
npm config get registry
-
然后下载 echarts:
npm install echarts -S
;package-lock.json 会更新。package-lock.json 部分前后对比图:左边是更新前的,右边是更新后的,主要的变化就是 resolved 字段从 npm.garenanow.com 变成了 registry.npmjs.org。node_modules 下 @babel/generator 的 package.json 部分截图:
node_modules 下 @babel/helper-annotate-as-pure 的 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 中有很多冲突的情况。
其他文章链接
[译文]一步步构建发布一个 TypeScript NPM 包