前言
项目中执行npm install
发生了什么,众所周知,执行npm install
时会在当前项目目录的node_modules
中安装依赖。并且将依赖项的层级关系保存在package-lock.json
中。那么依赖项的层级关系是怎么确认的?依赖项之间是否存在区别?有无package-lock.json
在安装时有什么区别呢?笔者希望借以此文可以和读者们一起厘清npm install
。
工欲善其事必先利其器,先一起来了解下npm install
。
npm-install
常用的命令包含(具体含义和执行效果会在下文说明)
npm install
npm install -P|--save-prod
npm install -D|--save-dev
npm install -O|--save-optional
npm install --no-save
aliases: npm i, npm add
npm install
这个命令会在项目路径下安装一个或多个依赖包,如果项目中存在package-lock
、npm-shrinkwrap
或yarn.lock
文件。那么安装过程将就基于这些文件。其优先级为:
npm-shrinkwrap.json
package-lock.json
yarn.lock
其中npm-shrinkwrap.json
是npm 5
之前的依赖锁定文件,其在npm 5
后被package-lock.json
替换。两个文件功能类似,最大的不同是npm-shrinkwrap.json
需要执行npm shrinkwrap
来初始化生成,而package-lock.json
为自动生成。本文撰写时npm
使用的版本为v6.14.5
,故下文所有样例均在此版本下做交流讨论。其他版本任何问题欢迎留言讨论。yarn.lock
是yarn cli
的产物,不在此文章讨论。
npm install
执行时会解析package.json
中定义的依赖关系,进而根据package.json
生成package-lock.json
依赖锁定文件。package.json
中的依赖关系可以由开发人员自行定义,也可使用npm install [--optional]
来指定存储位置,或使用npm install --no-save
不进行存储。
自古习物先习源。package.json
中是那些在影响依赖树呢?
package.json
package.json
文件是项目的描述文件,包括项目的name
、version
、description
等很多字段。但真正影响依赖树的并不多,包括dependencies
、devDependencies
、peerDependencies
、 optionalDependencies
、bundledDependencies
。下面一起来看下他们区别和使用场景。
dependencies:主要依赖
dependencies
是主要依赖。也即必须依赖。由包名和版本区间映射成的简单 json
对象组成。版本区间由一个或多个分隔符组成。是项目运行、打包的主要依赖。执行npm install
时会从上至下递归安装dependencies
及其内部依赖。并将解析的依赖树存储在package-lock.json
中。会随版本一起发布到npm
库。是最重要的依赖节点。
执行npm install -P|--save-prod
会将指定包放入此节点。
常用的dependencies
如:axios
、fetch
等。
{
"dependencies": {
"axios": "^0.21.1",
"fetch": "^1.1.0"
}
}
devDependencies:开发依赖
devDependencies
是开发环境的依赖。其格式同dependencies
,开发人员本地执行npm install
时也会从上至下递归安装devDependencies
及其内部依赖。并将解析的依赖树存储在package-lock.json
中。也会随版本一起发布到npm
库。但是不同的是。非开发人员通过项目安装依赖时。也就是包安装后出现在node_modules
中时,devDependencies
不会被安装,也不会出现在项目的package-lock.json
中。
所以在开发时,dependencies
和devDependencies
区别并不大,但是作为依赖包被其他项目引入时,只有dependencies
中的依赖会被解析成依赖树并下载。devDependencies
中的依赖会被忽略。
执行npm install -D|--save-dev
会将指定包放入此节点。
常用的devDependencies
如:karma
、mocha
、webpack
等
{
"devDependencies": {
"karma": "^1.3.0",
"mocha": "^5.2.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
}
}
peerDependencies:同等依赖
peerDependencies
是同等依赖。其格式同dependencies
。主要在开发包时使用,常规开发项目不建议使用。比如某些情况下开发人员为了表明在不同系统和运行环境下的兼容性,即非所有情况下都是必要的依赖。就会通过peerDependencies
来表明。peerDependencies
中更像是当前包所依赖是插件。peerDependencies
里面的依赖会随版本一起发布,但是不会自动安装,需要开发和使用人员手动安装。(npm v7
下会默认安装)
如eslint-plugin-prettier
会同等依赖eslint
和prettier
。即eslint-plugin-prettier
需要在eslint
和prettier
都安装时才会生效。但是其自身并不会强制安装eslint
和prettier
。而是会通过警告的方式提示开发者。
`eslint-plugin-prettier requires a peer of xxx but none is installed. You must install peer dependencies yourself`
如果不想在未安装peerDependencies
的情况下提示警告。可以通过peerDependenciesMeta
设置optional
为true
来关闭警告。
{
"peerDependencies": {
"eslint": ">=5.0.0",
"prettier": ">=1.13.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
}
}
optionalDependencies:可选依赖
optionalDependencies
是可选依赖。其格式同dependencies
。当需要某些依赖在安装失败时不会阻塞项目的运行和打包,就可以使用optionalDependencies
来定义。optionalDependencies
里面的依赖会随版本一起发布,且会自动安装。可以使用npm install --no-optional
命令来跳过安装。
执行npm install -O|--save-optional
会将指定包放入此节点。
optionalDependencies
使用需要开发者在项目中作兼容。如:
try {
var foo = require('foo')
var fooVersion = require('foo/package.json').version
} catch (er) {
foo = null
}
if ( notGoodFooVersion(fooVersion) ) {
foo = null
}
// .. then later in your program ..
if (foo) {
foo.doFooThings()
}
bundledDependencies:绑定依赖
bundledDependencies
是绑定依赖,其值是一个数组。在发包时定义绑定包。常规项目中使用的并不多。和npm install
的也并无关系,所以在此不做过多介绍。值得一提的是,使用bundleDependencies
也是可以的。
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized",
"super-streams"
]
}
了解了package.json
中影响依赖树的节点,那么接下来就是重头戏npm install
登场了~
一、无依赖冲突
最简单的场景莫过于此,当项目的package.json
的依赖及其子 依赖间没有冲突时,即A依赖B、C、D。表示为A[B、C、D]。则依赖会平铺在node_modules
下。即使有多个相同的依赖,只要版本不存在冲突,就都符合当前场景。
举个例子。fetch-demo2
项目中值依赖fetch
这一个包。
{
"name": "fetch-demo2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"fetch": "^1.1.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
此时执行npm i
,会得到如下的目录结构的node_modules
。
可得到依赖树(根据package-lock.json
分析得出)
不难得出以下结论。当项目中的依赖无冲突时,项目依赖及其内部依赖会平铺在一级node_modules
中。
二、项目顶级依赖存在冲突
顶级依赖即项目package.json
中的依赖。
当项目顶级依赖存在冲突时,会将顶级依赖放在node_modules
中的一级目录下,冲突的包放在自己的node_modules
下。为了模拟这种场景,将biskviit@2.0.0
放在fetch-demo2
项目的顶级依赖上。
{
"dependencies": {
"fetch": "^1.1.0",
"biskviit": "2.0.0"
}
}
执行npm install
后查看node_modules
的目录结构
分析依赖树
可以看出在顶级依赖biskviit@2.0.0
和内部依赖biskviit@1.0.1
存在冲突时,顶级依赖会占据node_modules
的一级目录,内部依赖则会存储在其内部的node_modules
中
三、项目内部依赖存在冲突
当项目的内部依赖存在冲突时,会先检测一级node_modules
是否存在依赖包,不存在则存储,如果存在判断是否有版本冲突,无冲突则使用一级node_modules
的依赖包,有冲突则存储在自身的node_modules
中。还是举例说明下。
一级node_modules
无冲突包
发布自定义新包conflict-lbywer@1.0.0
到npm
。其依赖为
{
"dependencies": {
"biskviit": "^2.0.0"
}
}
将fetch-demo2
项目的顶级依赖改为
{
"dependencies": {
"conflict-lbywer": "1.0.0",
"fetch": "^1.1.0"
}
}
删除node_modules
和package-lock.json
后,执行npm i
后,查看目录结构
分析依赖树
可以看出conflict-lbywer
所依赖的biskviit@2.0.0
和fetch
所依赖的biskviit@1.0.1
冲突时,在顶级依赖没有biskviit
的情况下,将biskviit@2.0.0
安装到了顶级依赖。
如果将顶级依赖中的conflict-lbywer
和fetch
更换顺序呢,依赖包顺序是否会发生变化,我们一起来研究。
修改顶级依赖
{
"dependencies": {
"fetch": "^1.1.0",
"conflict-lbywer": "1.0.0"
}
}
删除node_modules
和package-lock.json
后,执行npm i
后,查看目录结构
分析依赖树
可以看出在调整conflict-lbywer
和fetch
顺序后,目录结构并无变化。所以可得出结论,在一级node_modules
不存在冲突包时,会将高版本的包放在一级node_modules
中。低版本的放到内部的node_modules
中。
一级node_modules
有冲突包
如果一级node_modules
有冲突包时,情况又会如何呢?
删除顶级依赖中的conflict-lbywer
{
"dependencies": {
"fetch": "^1.1.0"
}
}
删除node_modules
和package-lock.json
后,执行npm i
后,查看目录结构
分析依赖树
顶级依赖添加conflict-lbywer@1.0.0
{
"dependencies": {
"fetch": "^1.1.0",
"conflict-lbywer": "1.0.0"
}
}
直接执行npm i
后,查看目录结构
分析依赖树
可以发现,在依赖树无变化的情况下,node_modules
的目录结构是不一样的。所以可以得出结论,在一级node_moudles
已经存在依赖包的情况下,新安装的依赖包如果存在冲突,会安装到内部的node_modules
中。
四、存在package-lock.json
这种情况也很简单,npm install
会完全按照package-lock.josn
的层级结构下载安装依赖包
删除node_moudles
后,执行npm install
直接执行npm install
后,查看目录结构
分析依赖树
可以发现,在存在package-lock.json
的情况下,node_modules
的目录结构是稳定的。
结语
上文对执行npm install
时,一些常见的情况做了测试和分析,也给出了相应的结论。欢迎读者们批评斧正。也欢迎打赏点赞哦~