npm模块安装机制

2021/9/3 NPM

参考地址:npm install的实现原理 (opens new window)

# npm模块安装机制

  • 发出npm install命令

  • 查询node_modules目录之中是否已经存在指定模块

    • 若存在,不再重新安装

    • 若不存在

      • npm向registry查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

# npm实现原理

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

  1. 执行工程自身preinstall

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

  2. 确定首层依赖模块

    首先需要做的是确定工程中的首层依赖,也就是dependencies和devDependencies属性中直接指定的模块(假设此时没有添加npm install参数)。工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  3. 获取模块

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

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

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

    node_modules
    -- foo
    
    -- bar
    
    -- lodash(保留的版本为兼容版本)
    
    1
    2
    3
    4
    5
    6

    假设version1和version2为非兼容版本,遇到的第一个会被放在根节点下,后面的版本保留在依赖树中:

    node_modules
    -- foo
    -- lodash@version1
    
    -- bar
    ---- lodash@version2
    
    1
    2
    3
    4
    5
    6
  5. 安装模块

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

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

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

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

TIPS

常见问题:

  • npm install xxx安装具体包时,为了保证构建的完整性和一致性,保证package.json和package.lock.json是正确的,所以都会先去遍历package.json,检查registry上每个依赖包是否存在。
  • npm install会执行npm prepublish的钩子:属于npm早期设计时的不足,官方也发现这里很容易让人迷惑,现在已经给出了废弃的提示:scripts | npm Documentation (opens new window),但是仍然会运行,npm ci时也会运行,可以用prepublishOnly替代。
  • 如果在packakge.json中指定依赖版本为"latest",但是如果同时存在package.lock.json,那除了第一次安装是最新版本,后续的版本还是会被锁定在lock文件里面的那个指定的版本。

# 先行版本

当要发布大版本或者核心的Feature时,但是又不能保证这个版本的功能100%正常。这个时候就需要通过发布先行版本。比较常见的先行版本包括:内测版、灰度版本了和RC版本。Semver规范中使用alpha、beta、rc(以前叫做gama)来修饰即将要发布的版本。它们的含义是:

  • alpha:内部版本。
  • beta:公测版本。
  • rc:即Release candiate,正式版本的候选版本。

比如:1.0.0-alpha.0、1.0.0-alpha.1、1.0.0-beta.0、1.0.0-rc.0、1.0.p-rc.1等版本。alpha、beta、rc后需要带上次数信息。

# 依赖版本号

在npm的依赖的规则中,还有~><=>=<=-||xX*等符号;当使用npm install XX时,被安装的依赖的版本号前会默认加上^符号。

  • ^:表示同一主版本号中,不小于指定版本号的版本号。
  • ~:表示同一主版本号和次版本号中,不小于指定版本号的版本号(大于等于修订版本号)。
  • ><=>=<=-:用来指定一个版本号范围,1.0.0 - 1.2.0,注意使用-的时候,必须两边都有空格。
  • ||:表示或。
  • xX*:表示通配符。
最近更新: 2025年03月13日 17:49:47