Skip to main content

npm install 工作原理

· 11 min read

介绍 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 installnpm 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.0C2@1.0.0package.json 的放置顺序则决定了 node_modules 的依赖结构:

C@1.0.0C2@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.0C2@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 installnpm update命令下载依赖后,除了将依赖包安装在node_modules 目录下外,还会在本地的缓存目录缓存一份。

通过 npm config get cache 命令可以查询到:在 LinuxMac 默认是用户主目录下的 .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 值是相同的,则确保下载的依赖是完整的,如果不同,则进行重新下载。

整体流程总结

image-20220120152207125

执行 npm install 之后:

  • 检查 .npmrc 配置文件有关 npm 的配置信息,比如给 registry 换源。优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件> 全局级的 .npmrc 文件 > npm 内置的 .npmrc 文件
  • 检查项目中是否有 package-lock.json 文件
    • 无该文件
      • 根据 package.jsondependencies/devDependencies 中的依赖,依次从 npm registry 中获取包的信息
      • 构建依赖树结构(扁平化构建)
      • 在缓存中依次查找依赖树中的每个包
        • 没有找到该包的缓存,则从 npm registry 中下载包的压缩包
        • 验证数据完整性,如果不通过则重新下载
        • 将下载好的包复制到 npm 缓存目录中
        • 按照依赖树结构解压到当前 node_modules
        • 根据依赖树结构生成 package-lock.json 文件
    • 有该文件
      • 检查 package.jsonpackage-lock.json 中是否有包版本冲突
        • 有冲突则会重新从 npm registry 中获取包信息,然后构建依赖树结构
        • 无冲突则不用重新构造依赖树结构,直接进入缓存查找,后续过程相同

Reference