npm install 如何⼯作
依赖管理是 npm 的核⼼功能,原理就是执⾏ npm install 时, 从 package.json 中的 dependencies, devDependencies 将依赖包安装到当前⽬录的 ./node_modules ⽂件夹中,使⽤者⽆需关注这个⽬录⾥的⽂件夹结构细节,只管在业务代码中引⽤依赖包即可。
⼀个 npm 版本号包含三个部分:MAJOR.MINOR.PATCH,
MAJOR 表⽰主版本号,当做了不兼容的 API 修改;
MINOR 表⽰次版本号,当做了向下兼容的功能性新增;
PATCH 表⽰修订号,当做了向下兼容的问题修正;
npm 2 到 npm 5 有哪些变化和改进?假设应⽤⽬录为 app, ⽤两个流⾏的包 webpack, nconf 作为依赖包做⽰例说明
npm2 - 嵌套安装
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 命令
├─┬ nconf@0.8.5
│ ├── async@1.5.2
│ ├── ini@1.3.5
│ ├── secure-keys@1.0.0
│ └── yargs@3.32.0
└─┬ webpack@1.15.0
├── acorn@3.3.0
├── async@1.5.2
├── clone@1.0.3
├── ...
├── optimist@0.6.1
├── tapable@0.1.10
├── uglify-js@2.7.5
├── watchpack@0.2.9
└── source-map@0.4.4
这样的⽬录结构优点在于层级结构明显,便于进⾏傻⽠式的管理:
例如新装⼀个依赖包,可以⽴即在第⼀层 node_modules 中看到⼦⽬录
在已知所需包名和版本号时,甚⾄可以从别的⽂件夹⼿动拷贝需要的包到 node_modules ⽂件夹中,再⼿动修改 package.json 中的依赖配置
要删除这个包,也可以简单地⼿动删除这个包的⼦⽬录,并删除 package.json ⽂件中相应的⼀⾏即可
但这样的⽂件结构也有很明显的问题:
对复杂的⼯程, node_modules 内⽬录结构可能会太深,导致深层的⽂件路径过长⽽触发 windows ⽂件系统中,⽂件路径不能超过 260 个字符长的错误
部分被多个包所依赖的包,很可能在应⽤ node_modules ⽬录中的很多地⽅被重复安装。随着⼯程规模越来越⼤,依赖树越来越复杂,这样的包情况会越来越多,造成⼤量的冗余。
webpack 和 nconf 都依赖 async 这个包,所以在⽂件系统中,webpack 和 nconf 的 node_modules ⼦⽬录中都安装了相同的 async 包,并且是相同的版本。
+-------------------------------------------+
| app/ |
+----------+------------------------+-------+
| |
| |
+----------v------+ +---------v-------+
| | | |
| webpack@1.15.0 | | nconf@0.8.5 |
| | | |
+--------+--------+ +--------+--------+
| |
+-----v-----+ +-----v-----+
+-----------+ +-----------+
npm 3 - 扁平结构
主要为了解决以上问题,npm 3 的 node_modules ⽬录改成了更加扁平状的层级结构。⽂件系统中 webpack, nconf, async 的层级关系变成了平级关系,处于同⼀级⽬录中。
+-------------------------------------------+
| app/ |
+-+---------------------------------------+-+
| |
| |
+----------v------+ +-------------+ +---------v-------+
| | | | | |
| webpack@1.15.0 | | async@1.5.2 | | nconf@0.8.5 |
| | | | | |
+-----------------+ +-------------+ +-----------------+
虽然这样⼀来 webpack/node_modules 和 nconf/node_modules 中都不再有 async ⽂件夹,但得益于 node 的模块加载机制,他们都可以在上⼀级 node_modules ⽬录中找到 async 库。所以 webpack 和 nconf 的库代码中 require('async') 语句的执⾏都不会有任何问题。
这只是最简单的例⼦,实际的⼯程项⽬中,依赖树不可避免地会有很多层级,很多依赖包,其中会有很多同名但版本不同的包存在于不同的依赖层级,对这些复杂的情况, npm 3 都会在安装时遍历整个依赖树,计算出最合理的⽂件夹安装⽅式,使得所有被重复依赖的包都可以去重安装。
npm ⽂档提供了更直观的例⼦:
假如 package{dep} 写法代表包和包的依赖,那么 A{B,C}, B{C}, C{D} 的依赖结构在安装之后的 node_modules 是这样的结构:
A
+-- B
+-- C
+-- D
这⾥之所以 D 也安装到了与 B C 同⼀级⽬录,是因为 npm 会默认会在⽆冲突的前提下,尽可能将包安装到较⾼的层级。
如果是 A{B,C}, B{C,D@1}, C{D@2} 的依赖关系,得到的安装后结构是:
这⾥是因为,对于 npm 来说同名但不同版本的包是两个独⽴的包,⽽同层不能有两个同名⼦⽬录,所以其中的 D@2 放到了 C 的⼦⽬录⽽另⼀个 D@1 被放到了再上⼀层⽬录。
npm 5 - package-lock ⽂件
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
version, resolved, integrity ⽤来记录包的准确版本号、安装源的、内容 hash,决定了要安装的包的准确“⾝份”信息
只关注⽂件中的 dependencies: {} 我们会发现, dependencies 的 JSON 配置层次结构与⽂件系统中 node_modules 的⽂件夹层次结构是完全对照的
只关注 requires: {} 字段又会发现,除最外层的 requires 属性为 true 以外, 其他层的 requires 属性都对应着这个包的 package.json ⾥记录的⾃⼰的依赖项
因为这个⽂件记录了 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 packageName@x.x.x
卸载某个模块,请执⾏如下命令,或者⼿⼀动删除 package.json 中记录
npm uninstall packageName
安装模块的确切版本:
npm install packageName -D/S --save-exact # 安装的版本号将会是精准的,版本号前⾯不会出现^~字符
通过上述的命令来管理依赖包,package.json 和 package-lock.json 中的版本号都将会随之更新。
我们在升级/卸载依赖包的时候,尽量通过命令来实现,避免⼿动修改 package.json 中的版本号,尤其不要⼿动修改 package-lock.json