依赖管理是 npm 的核⼼功能,原理就是执⾏ npm install
时, 从 package.json 中的 dependencies, devDependencies 将依赖包安装到当前⽬录的 ./node_modules ⽂件夹中,使⽤者⽆需关注这个⽬录⾥的⽂件夹结构细节,只管在业务代码中引⽤依赖包即可。
⼀个 npm 版本号包含三个部分:MAJOR.MINOR.PATCH,
npm 2 到 npm 5 有哪些变化和改进?假设应⽤⽬录为 app, ⽤两个流⾏的包 webpack, nconf 作为依赖包做⽰例说明
npm 2 在安装依赖包时,采⽤简单的递归安装⽅法。执⾏ npm install
后,npm 2 依次递归安装 webpack 和 nconf 两个包到 node_modules 中。执⾏完毕后,我们会看到 ./node_modules 这层⽬录只含有这两个⼦⽬录
node_modules/
├── nconf/
└── webpack/
进⼊更深⼀层 nconf 或 webpack ⽬录,将看到这两个包各⾃的 node_modules 中,已经由 npm 递归地安装好⾃⾝的依赖包。包括./node_modules/webpack/node_modules/webpack-core
,./node_modules/conf/node_modules/async
等等。⽽每⼀个包都有⾃⼰的依赖包,每个 包⾃⼰的依赖都安装在了⾃⼰的 node_modules 中。依赖关系层层递进,构成了⼀整个依赖树,这个依赖树与⽂件系统中的⽂件结构树刚好层层对应
最⽅便的查看依赖树的⽅式是直接在 app ⽬录下执⾏ npm ls
命令
[email protected]
├─┬ [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ └── [email protected]
└─┬ [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── ...
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└─┬ [email protected]
├── [email protected]
└── [email protected]
这样的⽬录结构优点在于层级结构明显,便于进⾏傻⽠式的管理:
但这样的⽂件结构也有很明显的问题:
webpack 和 nconf 都依赖 async 这个包,所以在⽂件系统中,webpack 和 nconf 的 node_modules ⼦⽬录中都安装了相同的 async 包,并且是相同的版本。
+-------------------------------------------+
| app/ |
+----------+------------------------+-------+
| |
| |
+----------v------+ +---------v-------+
| | | |
| [email protected] | | [email protected] |
| | | |
+--------+--------+ +--------+--------+
| |
+-----v-----+ +-----v-----+
|[email protected]| |[email protected]|
+-----------+ +-----------+
主要为了解决以上问题,npm 3 的 node_modules ⽬录改成了更加扁平状的层级结构。⽂件系统中 webpack, nconf, async 的层级关系变成了平级关系,处于同⼀级⽬录中。
+-------------------------------------------+
| app/ |
+-+---------------------------------------+-+
| |
| |
+----------v------+ +-------------+ +---------v-------+
| | | | | |
| [email protected] | | [email protected] | | [email protected] |
| | | | | |
+-----------------+ +-------------+ +-----------------+
虽然这样⼀来 webpack/node_modules 和 nconf/node_modules 中都不再有 async ⽂件夹,但得益于 node 的模块加载机制,他们都可以在上⼀级 node_modules ⽬录中找到 async 库。所以 webpack 和 nconf 的库代码中 require('async')
语句的执⾏都不会有任何问题。
这只是最简单的例⼦,实际的⼯程项⽬中,依赖树不可避免地会有很多层级,很多依赖包,其中会有很多同名但版本不同的包存在于不同的依赖层级,对这些复杂的情况, npm 3 都会在安装时遍历整个依赖树,计算出最合理的⽂件夹安装⽅式,使得所有被重复依赖的包都可以去重安装。
npm ⽂档提供了更直观的例⼦:
A
+-- B
+-- C
+-- D
这⾥之所以 D 也安装到了与 B C 同⼀级⽬录,是因为 npm 会默认会在⽆冲突的前提下,尽可能将包安装到较⾼的层级。
A
+-- B
+-- C
`-- D@2
+-- D@1
这⾥是因为,对于 npm 来说同名但不同版本的包是两个独⽴的包,⽽同层不能有两个同名⼦⽬录,所以其中的 D@2 放到了 C 的⼦⽬录⽽另⼀个 D@1 被放到了再上⼀层⽬录。
npm 5 发布于 2017 年,这⼀版本依然沿⽤ npm 3 之后扁平化的依赖包安装⽅式,此外最⼤的变化是增加了 package-lock.json ⽂件 package-lock.json 的作⽤是锁定依赖安装结构,相当于本次 install 的⼀个快照,如果查看这个 json 的结构,会发现与 node_modules ⽬录的⽂件层级结构是⼀⼀对应的。
以依赖关系为: app{webpack} 的 ‘app’ 项⽬为例, 其 package-lock ⽂件包含了这样的⽚段。
{
"name": "app",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
// ... 其他依赖包
"webpack": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-1.8.11.tgz",
"integrity": "sha1-Yu0hnstBy/qcKuanu6laSYtgkcI=",
"requires": {
"async": "0.9.2",
"clone": "0.1.19",
"enhanced-resolve": "0.8.6",
"esprima": "1.2.5",
"interpret": "0.5.2",
"memory-fs": "0.2.0",
"mkdirp": "0.5.1",
"node-libs-browser": "0.4.3",
"optimist": "0.6.1",
"supports-color": "1.3.1",
"tapable": "0.1.10",
"uglify-js": "2.4.24",
"watchpack": "0.2.9",
"webpack-core": "0.6.9"
}
},
"webpack-core": {
"version": "0.6.9",
"resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
"integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
"requires": {
"source-list-map": "0.1.8",
"source-map": "0.4.4"
},
"dependencies": {
"source-map": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
"requires": {
"amdefine": "1.0.1"
}
}
}
}
//... 其他依赖包
}
}
同样类型的⼏个字段嵌套起来,主要是 version, resolved, integrity, requires, dependencies
因为这个⽂件记录了 node_modules ⾥所有包的结构、层级和版本号甚⾄安装源,它也就事实上提供了 “保存” node_modules 状态的能⼒。只要有这样⼀个 lock ⽂件,不管在那⼀台 机器上执⾏ npm install 都会得到完全相同的 node_modules 结果。
package-lock 优化的场景:在从前仅仅⽤ package.json 记录依赖,由于 semver range 的机 制;⼀个⽉前由 A ⽣成的 package.json ⽂件,B 在⼀个⽉后根据它执⾏ npm install 所得到 的 node_modules 结果很可能许多包都存在不同的差异,虽然 semver 机制的限制使得同⼀ 份 package.json 不会得到⼤版本不同的依赖包,但同⼀份代码在不同环境安装出不同的依赖包,依然是可能导致意外的潜在因素。
npm 5 默认会在执⾏ npm install 后就⽣成 package-lock ⽂件,并且建议提交到代码库中。
在 npm 5.0 中,如果已有 package-lock ⽂件存在,若⼿动在 package.json ⽂件新增⼀条依赖,再执⾏ npm install, 新增的依赖并不会被安装到 node_modules 中, package-lock.json 也不会做相应的更新。这样的表现与使⽤者的⾃然期望表现不符。所以不要使⽤ 5.0。
npm 2.x/3.x 已成为过去式,在 npm 5.x 以上环境下(版本最好在 5.6 以上,因为在 5.0 ~5.6 中间对 package-lock.json 的处理逻辑更新过⼏个版本,5.6 以上才开始稳定)
当 package.json 和 package-lock.json 同时存在时,npm install 会去检测 package-lock.json 指定的依赖版本是否在 package.json 指定的范围内。如果在,则安装 package-lock.json 指定的版本。如果不在,则忽略 package-lock.json,并且⽤安装的新版本号覆盖 package-lock.json
在⼤版本相同的前提下,如果⼀个模块在 package.json 中的⼩版本要⼤于 package-lock.json 中的⼩版本,则在执⾏ npm install 时,会将该模块更新到⼤版本下的最新的版本,并将版本号更新⾄ package-lock.json。如果⼩于,则被 package-lock.json 中的版本锁定。
如果⼀个模块在 package.json 和 package-lock.json 中的⼤版本不相同,则在执⾏ npminstall 时,都将根据 package.json 中⼤版本下的最新版本进⾏更新,并将版本号更新⾄ package-lock.json。
如果⼀个模块在 package.json 中有记录,⽽在 package-lock.json 中⽆记录,执⾏ npminstall 后,则会在 package-lock.json ⽣成该模块的详细记录。同理,⼀个模块在 package.json 中⽆记录,⽽在 package-lock.json 中有记录,执⾏ npm install 后,则会在 package-lock.json 删除该模块的详细记录。
如果要更新某个模块⼤版本下的最新版本(升级⼩版本号),请执⾏如下命令:
npm update packageName
如果要更新到指定版本号(升级⼤版本号),请执⾏如下命令:
npm install [email protected]
卸载某个模块,请执⾏如下命令,或者⼿⼀动删除 package.json 中记录
npm uninstall packageName
安装模块的确切版本:
npm install packageName -D/S --save-exact # 安装的版本号将会是精准的,版本号前⾯不会出现^~字符
通过上述的命令来管理依赖包,package.json 和 package-lock.json 中的版本号都将会随之更新。
我们在升级/卸载依赖包的时候,尽量通过命令来实现,避免⼿动修改 package.json 中的版本号,尤其不要⼿动修改 package-lock.json。