Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sisterAn opened this issue Feb 25, 2019 · 14 comments

Comments

@sisterAn
Copy link
Collaborator

1. npm 模块安装机制:

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

2. npm 实现原理

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

  1. 执行工程自身 preinstall

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

  1. 确定首层依赖模块

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

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

  1. 获取模块

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

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

  1. 安装模块

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

  1. 执行工程自身生命周期

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

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

参考 npm 模块安装机制简介

详解npm的模块安装机制

npm install的实现原理

@yygmind yygmind changed the title 第20题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块? 第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块? Apr 26, 2019
@AlexZhong22c
Copy link

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

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

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

以上只是建议。

@houmao
Copy link

houmao commented Aug 15, 2019

image
当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到目前为止依然是没办法完全解决冗余的。

@yft
Copy link

yft commented Sep 28, 2019

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

@dujuncheng
Copy link

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

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

@yisense
Copy link

yisense commented Nov 14, 2019

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

@TheFoolG
Copy link

TheFoolG commented Jan 17, 2020

image
当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不是会去除冗余吗?

@Wxh16144
Copy link

Wxh16144 commented Apr 9, 2020

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

@zhoucumt
Copy link

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

@nkguoym
Copy link

nkguoym commented Nov 24, 2020

回答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,获取不同的节点,然后采用深度优先的原则获取依赖包。

@Kun8018
Copy link

Kun8018 commented Dec 12, 2020

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

@vin1992
Copy link

vin1992 commented Mar 3, 2021

'查询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 所在的目录,如果还没有,就抛出异常,如果有,就返回它。

@shifengdiy
Copy link

shifengdiy commented Aug 26, 2021

模块安装过程

  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. 重复此过程,直到顶层模块的嵌套依赖安装完毕

@Hilshire
Copy link

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

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

@Yangfan2016
Copy link

https://zhuanlan.zhihu.com/p/503702422

上面的讨论都 少一个对 bin 文件的处理过程
在安装时,npm 会将文件软链接/符号连接到 prefix/bin 以进行全局安装或./node_modules/.bin/本地安装

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests