先说结论:
npm i 和 npm ci 差不多,都是根据 package-lock.json > package.json 优先级安装。日常使用中比较大的差异:
npm i 可以安装单个依赖。
npm ci 可以自动删除 node_modules 目录,且不会改变 package-lock.json 文件,但执行时 package-lock.json 必须存在且版本号与 package.json 要求一致。
正文
关于我写这篇文章的起因二三事。
因为 3 月底上海疫情,突然通知就封校了,导致公司电脑没背回来,自己电脑的环境是很久之前配的了,有些项目能跑,有些项目拉了最新代码之后会报错,于是开始配置环境。但依赖下起来有点慢,于是打开文档想找找之前写的代理。突然发现,之前文档里写的npm i变成了npm ci,大概是同事改的,但该同事是大佬且是错别字警察,应该不会放任文档中有错别字的,于是我小小调查了下,什么是npm ci。
先看文档
npm ci 和 npm i 的差异
从文档描述来看,简言之就是 npm ci 和 npm i 差不多,只不过 npm ci:
- 它是一种更干净的安装;
- 它要求
package-lock.json等锁依赖版本的文件必须存在,且版本必须和package.json中要求的版本对应,否则会报错; - 它会在安装之前先把
node_modules文件夹(下文可能会称为 modules)删除; - 它无法安装单个依赖
对于一个前端草履虫的我来说,如果 build 项目出现依赖报错之类的问题,一般会使用 rm -rf node_modules && npm cache clean --force && npm i 重装依赖,因为很有可能是我 fetch 了主仓库的最新代码,而它们的依赖升级了,导致本地跑不起来。这时候大多会好起来,直到我遇到一个问题,就是主仓库有一个依赖要求版本是 latest ,且该依赖需要 npm 版本比我本地的更高,当时 npm i 未果,从而尝试了另一种奇葩的解决方法,就是装了个 nvm,先把这个包从 package.json 文件中注释掉,然后用低版本 npm i,再用高版本单独装这个包。(这么想想它是不是应该在 package.json 里就锁定版本比较好,而不是用 latest ?)其中装 nvm 也踩了不少坑 orz,之后可能会单独聊一下。
好奇宝宝时间
1. npm ci 有多 “干净”?
上文有提到,一般有依赖相关的问题,我会简单粗暴使用 rm -rf node_modules && npm cache clean --force && npm i 重装,基本 80% 的问题就好了。首先, rm -rf node_modules 删除了当前项目中的依赖包,其次,npm cache clean --force 删除了 npm 缓存,npm i 重新下载依赖。
从文档中可以看出,npm ci 只是删除了 node_modules,但是并没有删除 npm 缓存。虽然官方强调并不需要删除缓存,但难保你安装某些依赖的过程中如果因为网速镜像等一些问题报错而终止,之后的安装中会不会出现重复的问题。
感觉有点像你把移动硬盘的数据传电脑,传一半没电了,再次打开,可能电脑中有传一半的数据,但并不完整,算是已经损毁,根本无法打开。再次传的时候还是会提醒你文件已存在是否跳过,这时候你选择跳过的话,那你得到的就是一个坏掉的文件。当然我没看源码,纯属根据之前的经验瞎猜,如果有误,希望大佬们可以帮忙纠正~
tips:缓存位置可以使用 npm config get cache 看到,在 mac 下它是个不可见的文件夹 .npm,npm-cache 文档中有提到说,npm cache clean -f 删除的只是 _cacache 文件夹,发现有大佬使用它删除缓存失败的例子(请戳《记一次排错经历——npm缓存浅析》)可能有些缓存会放在 _cacache 外,可能还需要手动清理。
2. npm ci 为什么要求 package-lock.json 存在?package-lock.json 到底是什么?
身为草履虫的我一直认为,package-lock.json 是锁定项目中依赖版本的,而之前公司的项目中,lock 文件一直是 ignore 状态,因为不会被提交,所以很难注意到它何时被修改。直到后来发现 lock 不再被 ignore 了,npm i 之后,尽管 package.json 文件不会变,有时候 lock 文件会变,为什么呢?
下面,我们来简单说一下 npm i。
先放一张忘记从哪里扒的流程图
npm i 会根据 package-lock.json > package.json 顺序,以 package.json 为标准更新冲突依赖,更新结束后生成 package-lock.json 文件。
如果 modules 中原本存在符合要求版本的话会跳过安装,也就不会修改 package-lock.json。
本草履虫因为看不懂源码所以黑盒测试一下(别打我,以后会学会看源码的我保证!)。先随意找个本地有 package.json 有 lock 也有 modules 的项目,修改一下 modules 里某依赖 A 中的某个文件(于是菜鸡小心翼翼删掉了某一行注释)重新 npm i 一下,刚才那行注释并没有粗线。说明 npm i 并没有重新安装依赖。然后我们手动修改 package.json 里的某依赖 B 版本号,修改一下这个依赖的某个文件,再次 npm i ,这次可以看到刚才修改的依赖 B 中的文件有被覆盖,但最初修改的依赖 A 中的文件还是没有被覆盖。
再说一下 package-lock.json
package-lock.json 是你项目中所有依赖的描述,注意是【所有】依赖,如果说 package.json 中记录到你项目中用到的依赖 A,那 package-lock.json 中在记录了依赖 A 版本的同时,也记录了 A 要求的依赖 B 的版本,以及 A 描述中对于 B 的版本限制。
如果说 npm i 是根据 package-lock.json 安装,同时对比 package.json 中依赖版本,如果不同,则安装后更新 package-lock.json 文件;那么 npm ci 就是根据 package-lock.json 安装,同时对比 package.json,如果有不同,则停止安装并报错,即使成功,也不会因为依赖的依赖有变化而更新 package-lock.json 文件。
最后,举个栗子
已知你的项目中 package.json 中 A@^1.2.3。
1 个月前,你下载包的时候 A 是 1.2.3 版本,这时候 lock 里对于 A 和 B 的描述是酱紫的
"A": {
"version": "1.2.3",
...,
"requires": {
"B": "^3.5.1",
},
...
},
"B": {
"version": "3.5.1",
...
}
此时你查看 node_modules/A/package.json 是这样的
"name": "A",
"version": "1.2.3",
...,
"dependencies": {
"B": "^3.5.1",
}
node_modules/B/package.json 是这样的
"name": "B",
"version":"3.5.1",
...
。
。。
。。。
1 month later
。。。
。。
。
可能由于这段时间 A@1.2.3 中要求的依赖 B 版本发生变化,当你删除 node_modules npm i 之后,package-lock 对于 A 和 B 的描述会变为
"A": {
"version": "1.2.3",
...,
"requires": {
"B": "latest",
},
...
},
"B": {
"version": "3.5.1",
...
}
虽然使用 npm ci 后,package-lock.json 并不会发生变化,但不论是 npm i 还是 npm ci 后,查看 node_modules/A/package.json 都会变成
"name": "A",
"version": "1.2.3",
...,
"dependencies": {
"B": "latest",
}
node_modules/B/package.json 也并没有发生变化
"name": "B",
"version":"3.5.1",
...
唯一的变化大概就是
"name": "A",
"version": "1.2.3",
...,
"dependencies": {
- "B": "^3.5.1",
+ "B": "latest",
}
最后的最后,在我看来:
当 package-lock.json 与 package.json 没有冲突时:
rm -rf node_modules && npm i && git checkout -- package-lock.json 等于 npm ci,但有冲突时 npm ci 会感知到并且终止抛出异常。
所以感觉 npm ci 会更以 package-lock.json 为准,在锁版本方面也比 npm i 优秀一点。
但它有个问题,就是无法明确体现出依赖的依赖版本要求发生的变化。emmmm,会造成什么影响呢。。。不知道大佬们有没有遇到过。。。以后遇到再补充叭~