npm install如何⼯作

npm install 如何⼯作

依赖管理是 npm 的核⼼功能,原理就是执⾏ npm install 时, 从 package.json 中的 dependencies, devDependencies 将依赖包安装到当前⽬录的 ./node_modules ⽂件夹中,使⽤者⽆需关注这个⽬录⾥的⽂件夹结构细节,只管在业务代码中引⽤依赖包即可。

⼀个 npm 版本号包含三个部分:MAJOR.MINOR.PATCH,

  • MAJOR 表⽰主版本号,当做了不兼容的 API 修改;
  • MINOR 表⽰次版本号,当做了向下兼容的功能性新增;
  • PATCH 表⽰修订号,当做了向下兼容的问题修正;

npm 2 到 npm 5 有哪些变化和改进?假设应⽤⽬录为 app, ⽤两个流⾏的包 webpack, nconf 作为依赖包做⽰例说明

npm2 - 嵌套安装

npm 2 在安装依赖包时,采⽤简单的递归安装⽅法。执⾏ npm install 后,npm 2 依次递归安装 webpack 和 nconf 两个包到 node_modules 中。执⾏完毕后,我们会看到 ./node_modules 这层⽬录只含有这两个⼦⽬录

node_modules/
├── nconf/
└── webpack/

进⼊更深⼀层 nconf 或 webpack ⽬录,将看到这两个包各⾃的 node_modules 中,已经由 npm 递归地安装好⾃⾝的依赖包。包括./node_modules/webpack/node_modules/webpack-core ,./node_modules/conf/node_modules/async 等等。⽽每⼀个包都有⾃⼰的依赖包,每个 包⾃⼰的依赖都安装在了⾃⼰的 node_modules 中。依赖关系层层递进,构成了⼀整个依赖树,这个依赖树与⽂件系统中的⽂件结构树刚好层层对应
最⽅便的查看依赖树的⽅式是直接在 app ⽬录下执⾏ npm ls 命令

[email protected]
├─┬ [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ └── [email protected]
└─┬ [email protected]
  ├── [email protected]
  ├── [email protected]
  ├── [email protected]
  ├── ...
  ├── [email protected]
  ├── [email protected]
  ├── [email protected]
  ├── [email protected]
  ├── [email protected]
  └─┬ [email protected]
    ├── [email protected]
    └── [email protected]

这样的⽬录结构优点在于层级结构明显,便于进⾏傻⽠式的管理:

  1. 例如新装⼀个依赖包,可以⽴即在第⼀层 node_modules 中看到⼦⽬录
  2. 在已知所需包名和版本号时,甚⾄可以从别的⽂件夹⼿动拷贝需要的包到 node_modules ⽂件夹中,再⼿动修改 package.json 中的依赖配置
  3. 要删除这个包,也可以简单地⼿动删除这个包的⼦⽬录,并删除 package.json ⽂件中相应的⼀⾏即可

但这样的⽂件结构也有很明显的问题:

  1. 对复杂的⼯程, node_modules 内⽬录结构可能会太深,导致深层的⽂件路径过长⽽触发 windows ⽂件系统中,⽂件路径不能超过 260 个字符长的错误
  2. 部分被多个包所依赖的包,很可能在应⽤ node_modules ⽬录中的很多地⽅被重复安装。随着⼯程规模越来越⼤,依赖树越来越复杂,这样的包情况会越来越多,造成⼤量的冗余。

webpack 和 nconf 都依赖 async 这个包,所以在⽂件系统中,webpack 和 nconf 的 node_modules ⼦⽬录中都安装了相同的 async 包,并且是相同的版本。

+-------------------------------------------+
|                    app/                   |
+----------+------------------------+-------+
           |                        |
           |                        |
+----------v------+       +---------v-------+
|                 |       |                 |
|  [email protected] |       |   [email protected]   |
|                 |       |                 |
+--------+--------+       +--------+--------+
         |                         |
   +-----v-----+             +-----v-----+
   |[email protected]|             |[email protected]|
   +-----------+             +-----------+

npm 3 - 扁平结构

主要为了解决以上问题,npm 3 的 node_modules ⽬录改成了更加扁平状的层级结构。⽂件系统中 webpack, nconf, async 的层级关系变成了平级关系,处于同⼀级⽬录中。

           +-------------------------------------------+
           |                    app/                   |
           +-+---------------------------------------+-+
             |                                       |
             |                                       |
  +----------v------+    +-------------+   +---------v-------+
  |                 |    |             |   |                 |
  | [email protected]  |    | [email protected] |   |   [email protected]   |
  |                 |    |             |   |                 |
  +-----------------+    +-------------+   +-----------------+

虽然这样⼀来 webpack/node_modules 和 nconf/node_modules 中都不再有 async ⽂件夹,但得益于 node 的模块加载机制,他们都可以在上⼀级 node_modules ⽬录中找到 async 库。所以 webpack 和 nconf 的库代码中 require('async') 语句的执⾏都不会有任何问题。
这只是最简单的例⼦,实际的⼯程项⽬中,依赖树不可避免地会有很多层级,很多依赖包,其中会有很多同名但版本不同的包存在于不同的依赖层级,对这些复杂的情况, npm 3 都会在安装时遍历整个依赖树,计算出最合理的⽂件夹安装⽅式,使得所有被重复依赖的包都可以去重安装。

npm ⽂档提供了更直观的例⼦:

  • 假如 package{dep} 写法代表包和包的依赖,那么 A{B,C}, B{C}, C{D} 的依赖结构在安装之后的 node_modules 是这样的结构:
A
+-- B
+-- C
+-- D

这⾥之所以 D 也安装到了与 B C 同⼀级⽬录,是因为 npm 会默认会在⽆冲突的前提下,尽可能将包安装到较⾼的层级。

  • 如果是 A{B,C}, B{C,D@1}, C{D@2} 的依赖关系,得到的安装后结构是:
A
+-- B
+-- C
   `-- D@2
+-- D@1

这⾥是因为,对于 npm 来说同名但不同版本的包是两个独⽴的包,⽽同层不能有两个同名⼦⽬录,所以其中的 D@2 放到了 C 的⼦⽬录⽽另⼀个 D@1 被放到了再上⼀层⽬录。

npm 5 - package-lock ⽂件

npm 5 发布于 2017 年,这⼀版本依然沿⽤ npm 3 之后扁平化的依赖包安装⽅式,此外最⼤的变化是增加了 package-lock.json ⽂件 package-lock.json 的作⽤是锁定依赖安装结构,相当于本次 install 的⼀个快照,如果查看这个 json 的结构,会发现与 node_modules ⽬录的⽂件层级结构是⼀⼀对应的。
以依赖关系为: app{webpack} 的 ‘app’ 项⽬为例, 其 package-lock ⽂件包含了这样的⽚段。

{
  "name": "app",
  "version": "0.1.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    // ... 其他依赖包
    "webpack": {
      "version": "1.8.11",
      "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.8.11.tgz",
      "integrity": "sha1-Yu0hnstBy/qcKuanu6laSYtgkcI=",
      "requires": {
        "async": "0.9.2",
        "clone": "0.1.19",
        "enhanced-resolve": "0.8.6",
        "esprima": "1.2.5",
        "interpret": "0.5.2",
        "memory-fs": "0.2.0",
        "mkdirp": "0.5.1",
        "node-libs-browser": "0.4.3",
        "optimist": "0.6.1",
        "supports-color": "1.3.1",
        "tapable": "0.1.10",
        "uglify-js": "2.4.24",
        "watchpack": "0.2.9",
        "webpack-core": "0.6.9"
      }
    },
    "webpack-core": {
      "version": "0.6.9",
      "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
      "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
      "requires": {
        "source-list-map": "0.1.8",
        "source-map": "0.4.4"
      },
      "dependencies": {
        "source-map": {
          "version": "0.4.4",
          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
          "requires": {
            "amdefine": "1.0.1"
          }
        }
      }
    }
    //... 其他依赖包
  }
}

同样类型的⼏个字段嵌套起来,主要是 version, resolved, integrity, requires, dependencies

  • version, resolved, integrity ⽤来记录包的准确版本号、安装源的、内容 hash,决定了要安装的包的准确“⾝份”信息
  • 只关注⽂件中的 dependencies: {} 我们会发现, dependencies 的 JSON 配置层次结构与⽂件系统中 node_modules 的⽂件夹层次结构是完全对照的
  • 只关注 requires: {} 字段又会发现,除最外层的 requires 属性为 true 以外, 其他层的 requires 属性都对应着这个包的 package.json ⾥记录的⾃⼰的依赖项

因为这个⽂件记录了 node_modules ⾥所有包的结构、层级和版本号甚⾄安装源,它也就事实上提供了 “保存” node_modules 状态的能⼒。只要有这样⼀个 lock ⽂件,不管在那⼀台 机器上执⾏ npm install 都会得到完全相同的 node_modules 结果。

package-lock 优化的场景:在从前仅仅⽤ package.json 记录依赖,由于 semver range 的机 制;⼀个⽉前由 A ⽣成的 package.json ⽂件,B 在⼀个⽉后根据它执⾏ npm install 所得到 的 node_modules 结果很可能许多包都存在不同的差异,虽然 semver 机制的限制使得同⼀ 份 package.json 不会得到⼤版本不同的依赖包,但同⼀份代码在不同环境安装出不同的依赖包,依然是可能导致意外的潜在因素。

npm 5 默认会在执⾏ npm install 后就⽣成 package-lock ⽂件,并且建议提交到代码库中。

在 npm 5.0 中,如果已有 package-lock ⽂件存在,若⼿动在 package.json ⽂件新增⼀条依赖,再执⾏ npm install, 新增的依赖并不会被安装到 node_modules 中, package-lock.json 也不会做相应的更新。这样的表现与使⽤者的⾃然期望表现不符。所以不要使⽤ 5.0。

依赖版本升级

npm 2.x/3.x 已成为过去式,在 npm 5.x 以上环境下(版本最好在 5.6 以上,因为在 5.0 ~5.6 中间对 package-lock.json 的处理逻辑更新过⼏个版本,5.6 以上才开始稳定)
当 package.json 和 package-lock.json 同时存在时,npm install 会去检测 package-lock.json 指定的依赖版本是否在 package.json 指定的范围内。如果在,则安装 package-lock.json 指定的版本。如果不在,则忽略 package-lock.json,并且⽤安装的新版本号覆盖 package-lock.json

以^版本为例

  • 在⼤版本相同的前提下,如果⼀个模块在 package.json 中的⼩版本要⼤于 package-lock.json 中的⼩版本,则在执⾏ npm install 时,会将该模块更新到⼤版本下的最新的版本,并将版本号更新⾄ package-lock.json。如果⼩于,则被 package-lock.json 中的版本锁定。

  • 如果⼀个模块在 package.json 和 package-lock.json 中的⼤版本不相同,则在执⾏ npminstall 时,都将根据 package.json 中⼤版本下的最新版本进⾏更新,并将版本号更新⾄ package-lock.json。

  • 如果⼀个模块在 package.json 中有记录,⽽在 package-lock.json 中⽆记录,执⾏ npminstall 后,则会在 package-lock.json ⽣成该模块的详细记录。同理,⼀个模块在 package.json 中⽆记录,⽽在 package-lock.json 中有记录,执⾏ npm install 后,则会在 package-lock.json 删除该模块的详细记录。

  • 如果要更新某个模块⼤版本下的最新版本(升级⼩版本号),请执⾏如下命令:

    npm update packageName
    
  • 如果要更新到指定版本号(升级⼤版本号),请执⾏如下命令:

    npm install [email protected]
    
  • 卸载某个模块,请执⾏如下命令,或者⼿⼀动删除 package.json 中记录

    npm uninstall packageName
    
  • 安装模块的确切版本:

    npm install packageName -D/S --save-exact # 安装的版本号将会是精准的,版本号前⾯不会出现^~字符
    

通过上述的命令来管理依赖包,package.json 和 package-lock.json 中的版本号都将会随之更新。
我们在升级/卸载依赖包的时候,尽量通过命令来实现,避免⼿动修改 package.json 中的版本号,尤其不要⼿动修改 package-lock.json。

你可能感兴趣的:(npm,webpack,前端)