NPM包依赖管理

npm 是 Node.js 的包管理工具,在实际使用过程中,我们经常会用 npm install 命令安装依赖包。

npm 官方对包也做了定义,戳此进

  • a. 一个含有 package.json 文件描述的程序的文件夹
  • b. 一个含有(a)的 gzip 压缩文件
  • c. 一个指向(b)url 地址
  • d. 格式为<name>@<version>的指向官方源的字符串,符合定义(c),如果 gulp@4.0.1
  • e. 格式为<name>@<tag>的指向 d 的字符串,如果 gulp@latest
  • f. 格式为<name>的代表最新版本包的字符串,如 gulp
  • g. 一个符合条件(a)的远程 git url

在项目中安装依赖包时,默认会将依赖包的信息加入到 package.json 的 dependencies 模块去。其实除了常见的 dependencies 以外,package.json 文件还有其他几项依赖类型,罗列如下:

  • dependencies 生产环境依赖
  • devDependencies 开发环境依赖
  • optionalDependencies 可选依赖
  • peerDependencies 前置依赖
  • bundleDependencies 需要打包的依赖

我们可以通过 npm install 的参数来指定依赖包需要添加到哪个依赖块,通用可选参数: [-P|--save-prod|-D|--save-dev|-O|--save-optional] [-E|--save-exact] [-B|--save-bundle] [--no-save] [--dry-run]

  • -P | –save-prod:添加至 dependencies
  • -D | –save-dev:添加至 devDependencies
  • -O | –save-optional:添加至 optionalDependencies
  • -E | –save-exact:安装确切的包版本,而不带默认的版本匹配符号,如^或者~
  • -B | –save-bundle:添加至 bundleDependencies 和 dependencies 中
  • –no-save:不添加至 dependencies 中
  • –dry-run:报告安装的状态而不是真正执行安装

依赖介绍

dependencies

dependencies 中包括了所有项目中使用到的包,其他用户要使用此包就需要安装 dependencies 中所有的包。

devDependencies

还有一类插件是在开发环境中使用的,比如我们可能会用 jest 来测试,用 eslint 进行代码样式规范,用 mock.js 来 mock 数据等等。其他用户可以正常使用我们的包但不需要安装此类依赖包,那么我们就可以将这类包列入 devDependencies 中。

peerDependencies

通常情况下,npm 能够很好的管理我们的依赖包。假设我们的项目依赖PackageA@2.1.0,但是引用了依赖PackageA@1.9.0的包 PackageB,npm install 生成的依赖树如下:

1
2
3
├── PackageA@2.1.0
└─┬ PackageB@1.0.0
└── PackageA@1.9.0

通常情况下这不会有问题,PackageB 内会有他可以用的版本为 1.9.0 的 PackageA,同时并不会和项目的PackageA@2.1.0的冲突。

然而在插件的使用时我们有可能会遇到以下情况,,举个例子:项目中需要依赖包 gulp 版本为最新的@latest(4.0.2),而 gulp 插件 gulp-plugin-example 依赖的 gulp 版本为@3.2.0,此时我们会安装多个包 gulp。package.json 文件可能如下:

1
2
3
4
5
6
{
"dependencies":{
"gulp":"4.0.2"
"gulp-plugin-example":"0.1.1"
}
}

但这时候如果执行 npm install 可能会导致我们不期望的依赖树

1
2
3
├── gulp@4.0.2
└─┬ gulp-plugin-example@0.1.1
└── gulp@3.2.0

此时插件使用的和宿主环境的 gulp 版本不一致,由于 gulp-plugin-example 只是插件,我们并不想因此安装多次 gulp,且希望宿主环境去安装满足插件 peerDependencies 所指定依赖的包,然后在插件 import 或者 require 依赖包时,永远引用的是宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。

通常我们会用 peerDependencies 去表达:如果你要使用此插件,需要确保宿主环境的依赖关系版本为@xx.xx。拥有 peerDependencies 意味着你的包需要的依赖关系和其他安装此包的用户的现有依赖关系是需要保持一致的。所以在编写插件时,需要确定主依赖包的版本,并添加至插件 package.json 中的 peerDependencies 中。

1
2
3
4
5
6
{
"name": "gulp-plugin-example",
"peerDependencies": {
"gulp":"^4.0.0"
}
}

如果这时候执行 npm install gulp-plugin-example,gulp 包也会随之安装。如果之后尝试去安装另一个 gulp 插件且依赖只依赖 gulp 3.x 版本的话,将会报错,这也成功解决了插件引用不同依赖包版本的问题!

更新:npm 版本 1,2 和 7 会自动安装 peerDependencies 的依赖包如果其未在上层依赖树中明确。npm 版本 3 至 6,我们则会收到告警告知 peerDependencies 还未安装。

peerDependencies 依赖包的版本要求与常规的 dependencies 不同,需要更加广。使用过程中不应该把 peer dependencies 的锁定在固定的版本。如果两个插件依赖的版本不一样,而使用者又懒得花时间去检查实际上符合条件的共用最低版本,这就会很糟心。

最佳情况是根据semver规则决定 peerDependencies 的版本要求,合理使用^以及~去匹配版本。如果依赖 1.5.4 版本的特性,也可以使用范围去匹配,如:”>= 1.5.4 < 2”

optionalDependencies

如果一个依赖关系可以被使用,但是又希望 npm 在找不到他或者安装失败的情况下继续进行,那么其可以被放入 optionalDependencies 内。通常这对那些不是所有运行环境或机器都需要的依赖关系很有用。

tips:optionalDependencies 会覆盖 dependencies 中的同名依赖包,所以不要把包同时写进这两个字段中。

bundleDependencies

bundleDependencies 是一个存放在主包发布时也需要同时打包的包名的数组。

假设我们的有如下的包,执行 npm pack

1
2
3
4
5
6
7
8
9
10
{
"name": "test",
"version": "1.0.0",
"dependencies": {
"moment": "^2.29.3"
},
"bundleDependencies": [
"moment"
]
}

那么我们将会得到的 test-1.0.0.tgz 文件,执行 npm install test-1.0.0.tgz 后也会得到如下结构:

1
2
3
4
├─ node_modules
├── test
│ └── moment
└── xxx

package-lock.json 文件

package-lock.json 文件是 npm 修改了 node_modules 依赖树或者 package.json 文件自动生成的产物。它描述了生成的准确依赖树,以便后续安装或者其他人安装都能生成相同的树。

假想现有的 package.json 文件的依赖是 “react” :“^18.0.0”,因为有标识符^,所以如果 react 在 18 大版本下更新至 18.0.1,那么 npm install 的时候将会自动安装最新版本 18.0.1。这样可能就会让你本地的版本和其他人不一致,从而引起一些可能发生的相关问题。

package-lock.json 的出现就很好的解决此问题,他的目的就是锁定具体版本号,使每一个使用者安装的版本都保持一致。

npm install 时,node 会从 package.json 中读取到所有 dependencies 的信息与现有的 node_modules 模块进行对比,如果不存在的话就安装,并将依赖项确定的版本更新到 package-lock.json 文件中,不会更新其他依赖项版本;如果存在就检查更新,从 package-lock 文件中获取对应锁定的版本进行更新。

参考