介绍 npm install
工作原理
registry
npm 模块仓库提供了一个查询服务,叫做 registry 。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/
。
这个网址后面跟上模块名,就会得到一个 JSON 对象,里面是该模块所有版本的信息。比如,访问 https://registry.npmjs.org/react
,就会看到 react 模块所有版本的信息。
registry 网址的模块名后面,还可以跟上版本号或者标签,用来查询某个具体版本的信息。比如, 访问 https://registry.npmjs.org/react/v0.14.6 ,就可以看到 React 的 0.14.6 版。
返回的 JSON 对象里面,有一个dist.tarball
属性,是该版本压缩包的网址。
dist: {
shasum: '2a57c2cf8747b483759ad8de0fa47fb0c5cf5c6a',
tarball: 'http://registry.npmjs.org/react/-/react-0.14.6.tgz'
},
到这个网址下载压缩包,在本地解压,就得到了模块的源码。npm install
和npm update
命令,都是通过这种方式安装模块的。
npm 依赖管理的更新历程
嵌套模式
最早的 npm 版本在管理依赖时使用了一种很简单的方式。我们称之为嵌套模式,比如你的项目中有如下的依赖结构:
"dependencies": {
"A": "1.0.0",
"B": "1.0.0",
"C": "1.0.0"
}
同时 A、B、C 模块中都依赖有 D 模块,并且它们的版本并不全一致:
A@1.0.0 --> D@2.0.0
B@1.0.0 --> D@2.0.0
C@1.0.0 --> D@1.0.0
当我们执行 npm install
后,项目中的 node_modules
就会呈现如下结构:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── D@2.0.0
├── B@1.0.0
│ └── node_modules
│ └── D@2.0.0
└── C@1.0.0
└── node_modules
└── D@1.0.0
这种依赖管理简单明了,但存在很大的问题,除了 node_modules
目录长度的嵌套过深之外,还会造成相同的依赖存储多份的问题,比如上面的 D@2.0.0
就存放了两份,这明显也是一种浪费。
扁平模式
在 npm 3.x
之后采用了一种扁平化的模式,对于上述同样的依赖,执行完 npm install
之后,项目中的 node_modules
就会呈现如下扁平化结构(下面模块顺序是按照下载顺序排列):
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@2.0.0
└── C@1.0.0
└── node_modules
└── D@1.0.0
原理就是,安装模块时,不管是当前项目的直接依赖还是子依赖的依赖,都会优先安装到项目的第一层 node_modules
下,只有当遇到版本冲突时,才会将模块安装到某个子模块的 node_modules
下。
以上面的依赖为例,当我们执行 npm install
时,会先将 A 模块及其依赖的所有子模块(深度优先)全部下载到项目的 node_modules
下,然后继续对 B 模块、C 模块执行同样操作,当发现 C 模块下有一个 D@1.0.0
的模块,它和最外层的那个 D@2.0.0
的版本不一样,那么这个模块就单独在 C 模块的 node_modules
中安装。
虽然,npm v3 对依赖进行了扁平化,但是它也带来了依赖结构的不确定性问题。
比如,继续用上面的例子,我们再安装一个 C2@1.0.0 --> D@1.0.1
的模块,即 C2@1.0.0
的模块依赖了一个 D@1.0.1
的模块,此时项目中的 node_modules
就会呈现如下结构(模块顺序按照下载顺序排列):
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@2.0.0
└── C@1.0.0
└── node_modules
└── D@1.0.0
└── C2@1.0.0
└── node_modules
└── D@1.0.1
假如,A@1.0.0
模块没有依赖 D@2.0.0
模块,由于在执行 npm install
的时候,是按照 package.json
里依赖的顺序依次解析,则 C@1.0.0
和 C2@1.0.0
在 package.json
的放置顺序则决定了 node_modules
的依赖结构:
C@1.0.0
在 C2@1.0.0
前面时的依赖结构
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.0
└── C@1.0.0
└── C2@1.0.0
└── node_modules
└── D@1.0.1
C@1.0.0
在 C2@1.0.0
后面时的依赖结构
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.1
└── C2@1.0.0
└── C@1.0.0
└── node_modules
└── D@1.0.0
另外,为了让开发者在安全的前提下使用最新的依赖包,我们在 package.json
通常只会锁定大版本,这意味着在某些依赖包小版本更新后,同样可能造成依赖结构的改动,依赖结构的不确定性可能会给程序带来不可预知的问题。
important
扁平化模式另外还引出 "幽灵依赖" 和 "分身依赖",幽灵依赖是指由于依赖提升(扁平化),
扁平模式 + package-lock.json
为了解决 npm install
的不确定性问题,在 npm 5.x
版本新增了 package-lock.json
文件,而安装方式还沿用了 npm 3.x
的扁平化的方式。
package-lock.json
的作用是锁定依赖结构,即只要你目录下有 package-lock.json
文件,那么你每次执行 npm install
后生成的 node_modules
目录结构一定是完全相同的。
例如,我们有如下的依赖结构:
{
"name": "my-app",
"dependencies": {
"buffer": "^5.4.3",
"ignore": "^5.1.4",
"base64-js": "1.0.1",
}
}
执行 npm install
后生成的 package.json
如下:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"base64-js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz",
"integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg="
},
"buffer": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
},
"dependencies": {
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
}
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"ignore": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A=="
}
}
}
在 package.json
中最外层的 dependencies
字段的结构,和我们项目中的 node_modules
内的结构是一致的,内部对象的 key
对应的就是包名。
缓存机制
在执行 npm install
或 npm update
命令下载依赖后,除了将依赖包安装在node_modules
目录下外,还会在本地的缓存目录缓存一份。
通过 npm config get cache
命令可以查询到:在 Linux
或 Mac
默认是用户主目录下的 .npm/_cacache
目录。
在这个目录下又存在两个目录:
content-v2
用于存储tar
包的缓存- `
index-v5
用于存储tar
包的hash
npm 在执行安装时,可以根据 package-lock.json
中存储的 integrity、version、name
生成一个唯一的 key
对应到 index-v5
目录下的缓存记录,从而找到 tar
包的 hash
,然后根据 hash
再去找缓存的 tar
包直接使用。
npm
提供了几个命令来管理缓存数据:
npm cache add
:官方解释说这个命令主要是npm
内部使用,但是也可以用来手动给一个指定的 package 添加缓存。npm cache clean
:删除缓存目录下的所有数据,为了保证缓存数据的完整性,需要加上--force
参数。npm cache verify
:验证缓存数据的有效性和完整性,清理垃圾数据。
基于缓存数据,npm 提供了离线安装模式,分别有以下几种:
--prefer-offline
:优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载。--prefer-online
:优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块。--offline
:不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败。
文件完整性验证
在下载依赖包之前,我们一般就能拿到 npm
对该依赖包计算的 hash
值,用户下载依赖包到本地后,需要确定在下载过程中没有出现错误,所以在下载完成之后需要在本地在计算一次文件的 hash
值,如果两个 hash
值是相同的,则确保下载的依赖是完整的,如果不同,则进行重新下载。
整体流程总结
执行 npm install
之后:
- 检查
.npmrc
配置文件有关npm
的配置信息,比如给registry
换源。优先级为:项目级的.npmrc
文件 > 用户级的.npmrc
文件> 全局级的.npmrc
文件 > npm 内置的.npmrc
文件 - 检查项目中是否有
package-lock.json
文件- 无该文件
- 根据
package.json
的dependencies/devDependencies
中的依赖,依次从npm registry
中获取包的信息 - 构建依赖树结构(扁平化构建)
- 在缓存中依次查找依赖树中的每个包
- 没有找到该包的缓存,则从
npm registry
中下载包的压缩包 - 验证数据完整性,如果不通过则重新下载
- 将下载好的包复制到 npm 缓存目录中
- 按照依赖树结构解压到当前
node_modules
下 - 根据依赖树结构生成
package-lock.json
文件
- 没有找到该包的缓存,则从
- 根据
- 有该文件
- 检查
package.json
和package-lock.json
中是否有包版本冲突- 有冲突则会重新从
npm registry
中获取包信息,然后构建依赖树结构 - 无冲突则不用重新构造依赖树结构,直接进入缓存查找,后续过程相同
- 有冲突则会重新从
- 检查
- 无该文件