zl程序教程

您现在的位置是:首页 >  前端

当前栏目

浅析包管理工具对node_modules依赖处理的演进历史

Node依赖管理工具 处理 浅析 历史 演进 modules
2023-09-11 14:19:54 时间

一、npm 演进

  npm 从 v1 -v3- v5 版本的迭代都有重大的改变,一起来下看吧~。

1、npm v1 嵌套

  npm 在 v3 之前 node_modules 里的包都是嵌套的。

node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── D@1.0.0
    └── node_modules
        └── B@1.0.0

  随着项目越来越大,依赖包越来越多,这样也会带来一系列问题。

  • 嵌套的层级加深,文件路径过长。
  • 大量的包被重复安装。比如上面例子中的 B@1.0.0 就会被装两份。

2、npm v3 扁平

  在 v3 版本,实现了扁平化安装依赖的模式, node_modules 中的包成打平状态

node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
    └── node_modules
        └── B@2.0.0
├── D@1.0.0

  npm v3 的变化,虽然避免了嵌套过深以及重复安装的问题(但是需要注意的是多个版本的包只能有一个版本被提升),但是其存在很多不确定性(即生成的 node_modules 结构不确定)。

  假设 A@1.0.0 依赖 C@1.0.1, B@1.0.0 依赖 C@1.0.2,那么生成的 node_modules 结构什么样的呢?

node_modules
├── A@1.0.0
├── B@1.0.0
    └── node_modules
        └── C@1.0.2
├── C@1.0.1
// 还是下面的情况呢
node_modules
├── A@1.0.0
    └── node_modules
        └── C@1.0.1
├── B@1.0.0
├── C@1.0.2

  其实是都有可能,这就依赖于 A 和 B 在 package.json中的位置。

3、npm v5 扁平 + lock

  为了解决 node_modules 结构的不确定性,于是在 v5 版本中默认会生成 package-lock.json文件 。

  package-lock.json 文件可以帮我们记录安装的每一个包版本和其所依赖的其他包版本,这样在下一次安装的时候就可以通过这个文件来安装。由 package-lock.json 文件和 package.json 文件能确保始终得到一致的 node_modules 目录结构,这样就保证了安装依赖的确定性。

二、yarn

1、yarn 1

  yarn1 的出现是为了解决 npm v3 的问题,那时候还没有 npm v5。yarn install 生成的 node_modules 目录结构与 npm v5 相同,同时默认会生成一个 yarn.lock文件。只要 yarn 的版本相同,yarn 安装依赖的确定性就能保证。

  npm v5 中只需要 package-lock.json 就可以保正确的 node_modules 目录结构,而 yarn 需要同时拥有 yarn.lock 文件和 package.json文件

  在使用 yarn 作为包管理工具时,我们也需要主要以下几点:

  • yarn.lock 是自动生成的,不要手动修改它
  • 将 yarn.lock 文件上传到 git
  • 升级依赖时,使用yarn upgrade命令,不要手动修改 package.json 和 yarn.lock 文件
  • 不得以不要把 lock 文件删掉,整个重装。这样会造成原本锁住的版本都放开了,执行yarn install的时候会根据 package.json 里定义的版本区间去找最新版,可能会造成你预期外的依赖也被更新了, 有可能会引入 bug。

2、yarn2 版本是无 node_modules 模式,可以加快项目安装速度,同时大大缩减删除一整个项目的速度

三、pnpm

  pnpm(perfomance npm) 现代包管理工具,其性能上有很大的提高

1、基本使用

npm install -g pnpm // 全局安装 pnpm 
​
pnpm add axios // 添加至dependencies
pnpm add axios -D   // 添加至devDependencies
pnpm add -O [package] //保存到optionalDependencies
​
pnpm update  // 更新
​
pnpm remove/uninstall // 删除
​
pnpm dlx  // 从源中获取包而不将其安装为依赖项,热加载,并运行它公开的任何默认命令二进制文件。
​
pnpm link // 将本地项目连接到另一个项目,这里是硬连接。

2、基本特性

  • 本地安装包速度快: 相比于npm / yarn 快 2-3 倍
  • 磁盘空间利用高效: 不会重复安装同一个包
  • 安全性高:避免了npm/yarn 非法访问依赖即幽灵依赖和二重身的风险

四、pnpm 是如何提升性能的?

  一句话概括:pnpm 在安装依赖时使用了 hard link 机制,使得用户可以通过不同的路径去寻找某个文件。pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link。

  下面先简单讲讲几个概念: hard link 、symlink 以及全局的 store 目录。

1、什么是 hard link 和 symlink

  本质上都是文件访问的方式。

  hard link(硬链接):如果 A 是 B 的硬链接,则 A 的 indexNode(可以理解为指针) 与 B 的 indexNode 指向的是同一个。删除其中任何一个都不会影响另外一个的访问。作用是:允许一个文件拥有多个有效路径,这样用户可以避免误删。

  symlink(软链接或符号链接):类似于桌面快捷方式。比如 A 是 B 的软连接(A 和 B 都是文件名),A 和 B 的 indexNode 不相同,但 A 中只是存放这 B 的路径,访问 A 时,系统会自动找到 B。删掉 A 与 B 没有影响,相反删掉 B,A 依然存在,但它的指向是一个无效链接。

2、store 目录

  store 目录一般在${os.homedir}/.pnpm-store/v3/files 这个目录下。 由于 pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link,这样在不同项目中安装同一个依赖的时候,不需要每次都去下载,只需要安装一次就行,避免了二次安装的消耗。这点 npm 、yarn 在不同项目上使用,都需要重新下载安装。

  store 目录也会随着安装的包的数量越来越大,使用 pnpm store prune 命令可以删除不再被引用的包。(不推荐频繁使用)

3、pnpm 网状 + 平铺的 node_modules 结构

  我们同样使用 pnpm 来安装一下 swiper 包,此时会自动生成一个 pnpm-lock.yaml 文件。接着我们来看看在 pnpm 中 node_modules 结构与 npm 和 yarn 有什么不同。

  安装 swiper 包后,根 node_modules 下会存在两个目录:

  一个是 .pnpm 虚拟磁盘目录,用户不能直接从中 require;

  另一个 swiper 目录,正常 node require 的路径, 这个 swiper 我们称之为 swiper 的软链 。当 node 解析依赖时,会通过这个软链来找到 swiper 的真实位置,swiper 真实的位置在 .pnpm/swiper@8.0.7/node_modules/swiper 下,这个文件称为 swiper 的硬链,会真实的链接到全局的 store 中。

  由于兼容性问题,没有使用 symlink 代替 hard link 。实际上存在 store 目录里面的依赖也是可以通过软链接去找到的,node.js 本身提供了一个 --preserve-symlinks 的参数来支持 symlink ,但实际这个参数对应 symlink 的支持并不好,所以作者放弃了。

4、解决了 npm 与 yarn 的 共性问题

  npm 与 yarn 在安装依赖虽然也实现了包打平,但还是存在两个问题:phatomdoppelgangers

(1)phatom (非法访问依赖):package.json 中只声明了 A, A 的 depdencies 有 B, 这样安装在 A 时 B 也会被安装,项目中还是可以 require 到 B。

(2)doppelgangers (二重身): 一个包的不同版本还是会重复安装(不能打平同一个包的不同版本),能会造成同一个包重复安装,性能还是会损失。

  pnpm 中不会出现这两种情况:

  首先依赖的打平是在 .pnpm 的 node_modules 中,而.pnpm 是一个虚拟的磁盘目录,用户不能 require 到;

  其次 pnpm 安装的依赖始终都是存在全局 store 目录下的 hard links,一份不同的依赖始终都只会被安装一次。