第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

发布于 2022-08-29 02:50:09 字数 2684 浏览 164 评论 13

npm 模块安装机制

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。

模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules
-- foo
---- lodash@version1

-- bar
---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules
-- foo
-- bar
-- lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules
-- foo
-- lodash@version1

-- bar
---- lodash@version2

安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(13

各自安好 2022-05-04 13:56:34

当 lock file 存在时,如果 lock file 记录的版本信息不需要更新,则会使用 lock file 记录的模块信息。

详见:https://stackoverflow.com/a/53594050/8101803

红墙和绿瓦 2022-05-04 13:52:09

模块安装过程

  1. 查找此模块是否已经在node_modules中安装过,然后再.npm 缓存中查看是否有同版本缓存
  2. 如果有缓存,直接使用缓存
  3. 没有缓存去registry中按照registry/packagename/version模式搜索模块
  4. 将模块下载并解压到node_modules目录中,如果模块是可执行模块,将可执行文件放到 .bin 目录中
  5. 同时在模块放在 .npm 中作为副本缓存

依赖关系梳理

  1. 由于顶层模块下面可能依赖其他模块,模块之间存在嵌套依赖,packge.lock.json记录了模块的逻辑依赖树
  2. 如果完全安装逻辑依赖关系安装,会存在大量重复的包,npm会对安装做dudupe抹平处理,将重复的包安装在顶层
  3. 当模块安装时,先安装顶层模块,然后安装顶层模块的依赖模块,npm会检查顶层是否存在此模块,如果不存在将此模块安装在顶层
  4. 如果顶层已存在相同模块,检查版本是否符合要求-
    • 符合要求,不再安装模块,直接跳过
    • 不符合要求,在当前模块目录node_modeules下安装模块
  5. 重复此过程,直到顶层模块的嵌套依赖安装完毕
落在眉间の轻吻 2022-05-04 13:43:19

'查询node_modules目录之中是否已经存在指定模块' @sisterAn 请教一下,这里的查询,是只查询项目中package.json同级的目录呢还是会向父级目录逐层查询?

package.json 同级的目录 就会有 node_modules, @sisterAn 这里应该指的是所查询模块是否在当前目录下的node_modules, 如果在,就会在当前目录下找是否有package.json ,如果有package.json ,就会查找有没有index.js 文件,如果有就用index文件定义模块,如果没有index文件,还会检查package.json中是否有main元素,如果有那就用main元素指定的文件定义模块,如果没有就抛异常。

如果在当前目录下没有node_modules, 就会尝试进入父目录。如果父目录存在,就接着检查目录里是否有node_modules, 有则返回模块。

如果找了半天,父目录没有模块,或者干脆找到顶了,没有父目录了,这时就会是查找的最后一步,会检查由环境变量NODE_PATH 指定的目录下,也就是全局安装node_modules 所在的目录,如果还没有,就抛出异常,如果有,就返回它。

薄暮涼年 2022-05-04 13:34:41

之前面大厂的时候被问到这个问题,同事都说问的太细了,看来还是太嫩了

执妄 2022-05-04 13:32:03

回答npm install如何计算需要安装哪些包的。英文版原文来自:https://dev.to/shree_j/how-npm-works-internally-4012
npm install 的算法过程

  1. 检查node_modules 目录或者package-lock.json是否存在,并且根据目前的依赖关系构建一棵树 A,并且copy一个新树B。
    2.从package.json读出相关的依赖包,把这些依赖加到树B
  2. 比较树A和树B,获取不同的节点,然后采用深度优先的原则获取依赖包。
屌丝范 2022-05-04 12:30:00

'查询node_modules目录之中是否已经存在指定模块' @sisterAn 请教一下,这里的查询,是只查询项目中package.json同级的目录呢还是会向父级目录逐层查询?

倒数 2022-05-04 11:35:36

我也遇到这个问题,但是我还在苦苦寻找中

夜深人未静ゅ 2022-05-04 09:35:26


当A模块下的C v1.0模块被更新至C v2.0的前提下,我们可以通过npm dedupe把所有C v2.0的二级依赖模块“重定向”到一级目录下的那个C v1.0。
那如果A模块的C v1.0没更新到C v2.0,使用npm dedupe就没效果,那也就是说这个npm dedupe也只是在特定情况有效。也就是npm到目前为止依然是没办法完全解决冗余的。

那后面几个C V2.0不是会去除冗余吗?

紧拥背影 2022-05-04 06:30:42

1、发出npm install命令;
2、npm 向 registry 查询模块压缩包的网址;
3、下载压缩包,存放在~/.npm目录;
4、解压压缩包到当前项目的node_modules目录;

两相知 2022-05-04 04:44:20

上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。

有一个疑问,这里检查的是本地缓存.npm目录 还是 /node_modules目录 ?
貌似记得在 npm 中的某个版本中,即使 .npm目录 中存在缓存,npm 也会重新下载

三五鸿雁 2022-05-04 02:28:34

安装依赖的时候,获取完首层依赖,然后把每个依赖当做树的根节点,进行深度优先安装过程吗?还是是广度优先的?不同的安装方式,是不是对依赖版本冲突的解决也有影响?

幸福不弃 2022-05-03 19:27:59


当A模块下的C v1.0模块被更新至C v2.0的前提下,我们可以通过npm dedupe把所有C v2.0的二级依赖模块“重定向”到一级目录下的那个C v1.0。
那如果A模块的C v1.0没更新到C v2.0,使用npm dedupe就没效果,那也就是说这个npm dedupe也只是在特定情况有效。也就是npm到目前为止依然是没办法完全解决冗余的。

清浅ˋ旧时光ァ 2022-05-03 19:26:59

建议可以把原理性知识和整个流程分开说明,这样整个流程的顺序就很清晰了。

流程性的知识可以作为一个框架,然后慢慢填充细节内容到里面。

如果理解流程顺序不清晰,其实对于debug实际问题无益处。

以上只是建议。

~没有更多了~

关于作者

凉宸

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文