npm install
Background
假设此时你的repo中没有任何lock
文件,当你执行npm install
, npm会根据你在package.json
中对各种依赖的定义去安装这些依赖。安装完之后,会产生两个结果:
- node_modules: 所有依赖包
- npm-lock.json: lock 文件精确描述了 node_modules 目录中所列出的目录的物理树,这个文件相当于是node_module产生的物理树的快照。
package.json里面定义的是版本范围(比如^1.0.0),具体跑npm install的时候安的什么版本,要解析后才能决定,这里面定义的依赖关系树,可以称之为逻辑树(logical tree)。node_modules文件夹下才是npm实际安装的确定版本的东西,这里面的文件夹结构我们可以称之为物理树(physical tree)。安装过程中有一些去重算法,所以你会发现逻辑树结构和物理树结构不完全一样。
package-lock.json可以理解成对结合了逻辑树和物理树的一个快照(snapshot),里面有明确的各依赖版本号,实际安装的结构,也有逻辑树的结构。
去重算法
对复杂的工程,可能需要安装非常多的依赖包,就有可能出现,一个包被多个包依赖,很可能在应用 node_modules 目录中的很多地方被重复安装。随着工程规模越来越大,依赖树越来越复杂,这样的包情况会越来越多,造成大量的冗余。npm3以后,为了解决这个问题,将node_module
的包结构变成了变成了扁平的,因此可以避免安装很多重复的包。npm 3 都会在安装时遍历整个依赖树,计算出最合理的文件夹安装方式,使得所有被重复依赖的包都可以去重安装。
npm官方有专门讲过去重算法的问题:
npm中使用指令npm dedupe/npm ddp
, 搜索本地包树,并尝试通过将依赖项向上移动到树的更高层来简化整个结构,在树的更高层,多个依赖包可以更有效地共享依赖项。
举个例子:
a
+-- b <-- depends on c@1.0.x
| `-- c@1.0.3
`-- d <-- depends on c@~1.0.9
`-- c@1.0.10
在这种情况,npm run dedup
可以将树变成以下的状态:
a
+-- b
+-- d
`-- c@1.0.10
解释:
- b依赖c
- d依赖c
而且bd都依赖的c都是兼容的,因此可以使用同一份package。
根据npm去重原则,会将c上移一个层级方便bd共享。
- npm将每个依赖项尽可能地向上移动到树的根部,即使这个包不一定被多个包依赖(假设只有一个包依赖)。这样做将产生一个扁平的和去复制的树。
- 如果这个包A只被一个包依赖,那么尽可能将A向树的高层级移动。
- 如果在树的目标位置已经存在一个合适的版本,那么它将保持原样,但是其他副本将被删除。
- 但是每个层级只能有一个类型的包(不同版本也不行)
实例1:
项目中安装了webpack@1.15.0
,产生的package-lock
如下
"webpack": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz",
"integrity": "sha1-T/MfU9sDM55VFkqdRo7gMklo/pg=",
"requires": {
"acorn": "^3.0.0",
"async": "^1.3.0",
"clone": "^1.0.2",
"enhanced-resolve": "~0.9.0",
"interpret": "^0.6.4",
"loader-utils": "^0.2.11",
"memory-fs": "~0.3.0",
"mkdirp": "~0.5.0",
"node-libs-browser": "^0.7.0",
"optimist": "~0.6.0",
"supports-color": "^3.1.0",
"tapable": "~0.1.8",
"uglify-js": "~2.7.3",
"watchpack": "^0.2.1",
"webpack-core": "~0.6.9"
}
}
首先可以看到的是,webpack只有require
,require中描述的是webpack中package.json
的结构,但是没有dependency
,说明webpack自己的node_module
中啥也没有。
- require:除最外层的 requires 属性为 true 以外, 其他层的 requires 属性都对应着这个包的 package.json 里记录的自己的依赖项
- dependency: dependencies 层次结构与文件系统中 node_modules 的文件夹层次结构是完全对照的,这个字段可以理解成是node_module的快照
那么webpack的依赖,比如acorn
去哪了?发现他出现在根结构下
{
"name": "npm",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
},
根据查找,发现整个树上只有webpack
依赖这个包,根据
npm将每个依赖项尽可能地向上移动到树的根部,即使这个包不一定被多个包依赖(假设只有一个包依赖)。这样做将产生一个扁平的和去复制的树。
这个包acorn就被移到了树的根部。
实例二:
如果是 A{B,C}, B{C,D@1}, C{D@2} 的依赖关系,得到的安装后结构是:
A
+-- B
+-- C
`-- D@2
+-- D@1
- A依赖C,B也依赖C,两个C兼容,因此C被上移到和B一个层级。
- B依赖D1,C依赖D2,但是两个D不兼容,因此必须安装两个D。
如果这个包A只被一个包依赖,那么尽可能将A向树的高层级移动。
此时D1只被B依赖,因此D1被提高到和B一个层级
- 如果在树的目标位置已经存在一个合适的版本,那么它将保持原样,但是其他副本将被删除。
- 但是每个层级只能有一个类型的包(不同版本也不行)
此时D2只能留在原来的层级,因此BC层级已经有一个D
npm install工作流程
常常我都会有一个疑问,npm install的时候到底是根据package.json
安装呢?还是根据package-lock安装?
因此我做了一个小实验,安装webpack (npm: 6.7.0/node: v11.13.0):
package.json(before) | package-lock.json(before) | node_module(before) | command | package.json(after) | package-lock.json(after) | node_module(after) | |
---|---|---|---|---|---|---|---|
S1 | ^1.8.0 | 1.8.0 | null | install | ^1.8.0 | 1.8.0 | 1.8.0 |
S2 | ^1.8.0 | 1.8.0 | 1.8.0 | install | ^1.8.0 | 1.8.0 | 1.8.0 |
S3 | ^1.8.0 | 1.8.0 | 1.8.0 | up | ^1.15.0 | 1.15.0 | 1.15.0 |
S4 | ^1.8.0 | 1.15.0 | 1.15.0/null | install | ^1.8.0 | 1.15.0 | 1.15.0 |
s5 | ^1.8.0 | 3.0.0 | null | install | ^1.8.0 | 1.15.0 | 1.15.0 |
s6 | ^3.0.0 | 1.15.0 | null | install | ^3.0.0 | 3.12.0 | 3.12.0 |
summary:
-
npm install首先肯定是希望根据
package.json
文件安装package。-
如果
package-lock.json
文件中的版本和package.json
版本不能匹配(兼容)npm会根据package.json允许的package的最高版本安装package并且同时修改package-lock
-
如果
package-lock.json
文件中的版本和package.json
版本匹配(兼容)。比如s4的情况,一定会根据package-lock安装
-
- 最后安装的版本一定和package-lock中的一定一致
根据优先级来看:
npm安装的包一定是遵循package.json中的要求,如果package-lock中版本满足条件,那么完全按照lock中安装,如果不满足要求,安装满足要求的最高版本并且更新lock文件。