前言

项目中执行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-locknpm-shrinkwrapyarn.lock文件。那么安装过程将就基于这些文件。其优先级为:

  • npm-shrinkwrap.json
  • package-lock.json
  • yarn.lock

其中npm-shrinkwrap.jsonnpm 5之前的依赖锁定文件,其在npm 5后被package-lock.json替换。两个文件功能类似,最大的不同是npm-shrinkwrap.json需要执行npm shrinkwrap来初始化生成,而package-lock.json为自动生成。本文撰写时npm使用的版本为v6.14.5,故下文所有样例均在此版本下做交流讨论。其他版本任何问题欢迎留言讨论。yarn.lockyarn 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文件是项目的描述文件,包括项目的nameversiondescription等很多字段。但真正影响依赖树的并不多,包括dependenciesdevDependenciespeerDependenciesoptionalDependenciesbundledDependencies。下面一起来看下他们区别和使用场景。

dependencies:主要依赖

dependencies是主要依赖。也即必须依赖。由包名和版本区间映射成的简单 json对象组成。版本区间由一个或多个分隔符组成。是项目运行、打包的主要依赖。执行npm install时会从上至下递归安装dependencies及其内部依赖。并将解析的依赖树存储在package-lock.json中。会随版本一起发布到npm库。是最重要的依赖节点。

执行npm install -P|--save-prod会将指定包放入此节点。

常用的dependencies如:axiosfetch等。

{
  "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中。

所以在开发时,dependenciesdevDependencies区别并不大,但是作为依赖包被其他项目引入时,只有dependencies中的依赖会被解析成依赖树并下载。devDependencies中的依赖会被忽略。

执行npm install -D|--save-dev会将指定包放入此节点。

常用的devDependencies如:karmamochawebpack

{
  "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会同等依赖eslintprettier。即eslint-plugin-prettier需要在eslintprettier都安装时才会生效。但是其自身并不会强制安装eslintprettier。而是会通过警告的方式提示开发者。


`eslint-plugin-prettier requires a peer of xxx but none is installed. You must install peer dependencies yourself`

如果不想在未安装peerDependencies的情况下提示警告。可以通过peerDependenciesMeta设置optionaltrue来关闭警告。

{
  "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



npm安装axios卡住_npm安装axios卡住

可得到依赖树(根据package-lock.json分析得出)



npm安装axios卡住_npm安装axios卡住_02

不难得出以下结论。当项目中的依赖无冲突时,项目依赖及其内部依赖会平铺在一级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的目录结构



npm安装axios卡住_mysql_03

分析依赖树

npm安装axios卡住_编程语言_04

可以看出在顶级依赖biskviit@2.0.0和内部依赖biskviit@1.0.1存在冲突时,顶级依赖会占据node_modules的一级目录,内部依赖则会存储在其内部的node_modules

三、项目内部依赖存在冲突

当项目的内部依赖存在冲突时,会先检测一级node_modules是否存在依赖包,不存在则存储,如果存在判断是否有版本冲突,无冲突则使用一级node_modules的依赖包,有冲突则存储在自身的node_modules中。还是举例说明下。

一级node_modules无冲突包

发布自定义新包conflict-lbywer@1.0.0npm。其依赖为

{
  "dependencies": {
    "biskviit": "^2.0.0"
  }
}

fetch-demo2项目的顶级依赖改为

{
  "dependencies": {
    "conflict-lbywer": "1.0.0",
    "fetch": "^1.1.0"
  }
}

删除node_modulespackage-lock.json后,执行npm i后,查看目录结构



npm安装axios卡住_编程语言_05

分析依赖树



npm安装axios卡住_mysql_06

可以看出conflict-lbywer所依赖的biskviit@2.0.0fetch所依赖的biskviit@1.0.1冲突时,在顶级依赖没有biskviit的情况下,将biskviit@2.0.0安装到了顶级依赖。

如果将顶级依赖中的conflict-lbywerfetch更换顺序呢,依赖包顺序是否会发生变化,我们一起来研究。

修改顶级依赖

{
  "dependencies": {
    "fetch": "^1.1.0",
    "conflict-lbywer": "1.0.0"
  }
}

删除node_modulespackage-lock.json后,执行npm i后,查看目录结构



npm安装axios卡住_npm安装axios卡住_07

分析依赖树



npm安装axios卡住_npm安装axios卡住_08

可以看出在调整conflict-lbywerfetch顺序后,目录结构并无变化。所以可得出结论,在一级node_modules不存在冲突包时,会将高版本的包放在一级node_modules中。低版本的放到内部的node_modules中。

一级node_modules有冲突包

如果一级node_modules有冲突包时,情况又会如何呢?

删除顶级依赖中的conflict-lbywer

{
  "dependencies": {
    "fetch": "^1.1.0"
  }
}

删除node_modulespackage-lock.json后,执行npm i后,查看目录结构



npm安装axios卡住_编程语言_09

分析依赖树



npm安装axios卡住_编程语言_10

顶级依赖添加conflict-lbywer@1.0.0

{
  "dependencies": {
    "fetch": "^1.1.0",
    "conflict-lbywer": "1.0.0"
  }
}

直接执行npm i后,查看目录结构



npm安装axios卡住_java_11

分析依赖树



npm安装axios卡住_python_12

可以发现,在依赖树无变化的情况下,node_modules的目录结构是不一样的。所以可以得出结论,在一级node_moudles已经存在依赖包的情况下,新安装的依赖包如果存在冲突,会安装到内部的node_modules中。

四、存在package-lock.json

这种情况也很简单,npm install会完全按照package-lock.josn的层级结构下载安装依赖包

删除node_moudles后,执行npm install

直接执行npm install后,查看目录结构



npm安装axios卡住_java_13

分析依赖树



npm安装axios卡住_npm安装axios卡住_14

可以发现,在存在package-lock.json的情况下,node_modules的目录结构是稳定的。

结语

上文对执行npm install时,一些常见的情况做了测试和分析,也给出了相应的结论。欢迎读者们批评斧正。也欢迎打赏点赞哦~