前端包管理器的发展以及Npm、Yarn和Pnpm对比

ops/2025/2/15 12:08:46/

在现代前端开发中,包管理器是不可或缺的核心工具。随着 JavaScript 生态的快速发展,开发者经历了从 npm 一统天下到 Yarn 挑战格局,再到 pnpm 创新突破的技术演进。这里将对三种主流包管理器(npm/Yarn/pnpm)进行全方位对比,分析其设计原理、性能表现和适用场景。

剖析package.json

现代前端工程中不管使用那种打包工具,都一定有一个该项目的描述文件,这个文件就是package.json,这个文件有很多描述当前工程的字段,我将其汇总如下图展示:
在这里插入图片描述
如上面图示,必填的字段有nameversion,这两个属性组成一个npm模块的唯一标识

npm install原理详解

npm的发展

npm@2

在早期的npm(npm2)安装依赖时,处理方式非常的简单粗暴,就是以递归的形式,严格按照package.json的结构以及子依赖包的package.json结构进行安装,直至子依赖包不在依赖其他模块。比如说有一个项目要-app依赖了两个模块moduleA 和moduleB,而模块A是一个纯js模块不依赖其他模块,但moduleB又依赖了moduleB1和moduleB2那最后的目录结构就是这样的:
在这里插入图片描述
这样的方式优点是层级结构明显,node_module的结构和package.json的目录结构一致,并且可以保证每次安装的目录结构都是一样的。但是如果我们的项目非常的庞大,依赖的模块很多很复杂,那么嵌套的层就会非常的深,如下图所示

在这里插入图片描述
除此之外,如果不同层级的模块依赖了相同的模块,就会导致大量的冗余。在windows系统中,文件路径的最大长度是260个字符,如果层级过深就会导致不可预知的问题。

npm@3

为了解决上面的问题,NPM在3.x版本中做了一次比较大的更新。将嵌套的结构改成扁平的结构,安装模块时不管是直接依赖还是子依赖的依赖,优先将其安装在node_module目录下。还是上面的例子,安装之后我们会得到下面的目录结构
在这里插入图片描述
如果此时我们有新的模块依赖了moduleB2的某个版本此时就会检查,如果当前的moduleB2的版本符合我们的要求就会跳过,否则就会在当前模块下的node_module下安装新的moduleB2的模块。如下图所示:
在这里插入图片描述
这样不仅没有完全解决老的问题(依赖还是会嵌套的越来越深)并且还会引入一个新的问题,由于在执行npm install 时是按照package.json的依赖顺序依次解析的,如果moduleAmoduleB的解析顺序就决定了目录结构,如果此时先解析了moduleA那么目录结构可能是这样的:
在这里插入图片描述
此外,我们在实际开发中有时为了使用最新版本只会锁定大的版本,这就导致某些依赖的小版本更新后也会造成依赖的改动,这种依赖结构的不确定性可能会给程序带来不可预知的问题。

npm@5+

为了解决npm install不确定性的问题,在npm@5的这个版本中新增了一个package-lock.json文件,而安装的方式还是沿用了之前npm3的扁平方式。但是因为有了package-lock.json可以锁定依赖结构的,只要项目目录下有这个文件,每次执行npm install后生成的node_module结构一定都是相同的。
package.lock.json的结构如下:
在这里插入图片描述
最外面的两个属性 name 、version 同 package.json 中的 name 和 version ,用于描述当前包名称和版本。
dependencies 是一个对象,对象和 node_modules 中的包结构一一对应,对象的 key 为包名称,值为包的一些描述信息:

  • version:包版本 —— 这个包当前安装在 node_modules 中的版本;
  • resolved:包具体的安装来源;
  • integrity:包 hash 值,基于 Subresource Integrity 来验证已安装的软件包是否被改动过、是否已失效;
  • requires:对应子依赖的依赖,与子依赖的 package.json 中 dependencies的依赖项相同。;
  • dependencies:结构和外层的 dependencies 结构相同,存储安装在子依赖 node_modules 中的依赖包‘’
    这里注意,并不是所有的子依赖都有 dependencies 属性,只有子依赖的依赖和当前已安装在根目录的 node_modules 中的依赖冲突之后,才会有这个属性。

其他npm6+

在这之后npm也一直在更新,比如npm6引入的npm audit,npm7的workspaces和peer依赖自动安装,这里汇总后续的每个npm的特点如下:

版本最大特点
npm 5引入 package-lock.json,提升安装速度和稳定性
npm 6npm audit 引入安全性检查,支持 npm ci,提升性能
npm 7引入工作空间支持、自动安装 peerDependencies,全新锁文件格式
npm 8性能优化、错误处理改进、增强的安全性功能
npm 9性能提升、增强的工作空间支持、npm exec 命令和更好的漏洞检测
使用建议

所以我们一般在项目应用开发中一般把package-lock.json文件提交,固定项目的文件结构,以避免带来不必要的麻烦。
当我们开发一个npm工具包时,我们开发的npm包是要被其他应用系统集成进去的,就像上面我们说到的扁平机制,如果锁定了依赖包的版本就没有办法和其他模块的依赖包共享符合版本号的依赖了,就会造成不必要的冗余,所以此时不应该把package-lock.json发布出去(npm默认也是不会吧package-lock.json发布出去就是这个原因)

npm缓存机制

当我们执行npm install npm update命令下载依赖后,如果这台机器第一次安装这个模块,除了会将依赖包安装在node_module中之外,也会在本地的缓存目录中缓存一份。我们可以通过npm config get cache查询缓存目录,比如在mac下可以看到如下:
在这里插入图片描述
在这个目录下又存在两个目录:content-v2、index-v5,content-v2 目录用于存储 tar包的缓存,而index-v5目录用于存储tar包的 hash。
npm也提供了一些命令来管理缓存数据

  • npm cache add:官方解释说这个命令主要是 npm 内部使用,但是也可以用来手动给一个指定的 package 添加缓存;
  • npm cache clean:删除缓存目录下的所有数据,为了保证缓存数据的完整性,需要加上 --force 参数;
  • npm cache verify:验证缓存数据的有效性和完整性,清理垃圾数据;
    基于缓存数据,npm 提供了离线安装模式,分别有以下几种:
  • –prefer-offline: 优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载;
  • –prefer-online: 优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块;
  • –offline: 不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败;
文件完整性

在我们下载依赖包结束时会进行文件完整性的校验,以此来确保我们下载的包是完整的。那这个过程是怎样的呢?
在下载依赖包之前,一般都会拿到npm对该依赖包计算的hash值,我们可以通过npm info来查看某个文件包的hash值,比如我们查看一下axios的信息:
在这里插入图片描述
当用户下载完成时,会在本地计算一次文件的hash值如果两个hash值相同则说明下载的依赖包是完整的,如果不同就会重新下载。

完整流程

  • 检查 .npmrc 文件:优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件> 全局级的 .npmrc文件 > npm 内置的 .npmrc 文件;

  • 检查项目中有无 lock 文件;

  • 无 lock 文件:

    • 从 npm 远程仓库获取包信息;
    • 根据 package.json 构建依赖树,构建过程:
      • 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在 node_modules 根目录;
      • 当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下放置该模块;
      • 注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包;
    • 在缓存中依次查找依赖树中的每个包
      • 不存在缓存:
        • 从 npm 远程仓库下载包;
        • 校验包的完整性;
        • 校验不通过:
          • 重新下载;
        • 校验通过:
          • 将下载的包复制到 npm 缓存目录;
          • 将下载的包按照依赖结构解压到 node_modules;
      • 存在缓存:将缓存按照依赖结构解压到 node_modules;
    • 将包解压到 node_modules;
    • 生成 lock 文件;
  • 有 lock 文件:

    • 检查 package.json 中的依赖版本是否和 package-lock.json 中的依赖有冲突;
    • 如果没有冲突,直接跳过获取包信息、构建依赖树过程,开始在缓存中查找包信息,后续过程相同

整个过程如下图所示:
在这里插入图片描述

Yarn

yarn (Yet Another Resource Navigator)是 Facebook 开发的一个新的包管理器。它的开发是为了提供 NPM 当时缺乏的更高级的功能(例如版本锁定,但后续的功能npm都补充上了),同时也使其更安全、更可靠和更高效。
Yarn 现在更像是 NPM 的替代品,由于 Yarn 没有预装 Node.js,因此需要显式安装:

npm install yarn -g 

yarn的特点:

  1. 即插即用:从 Yarn 版本 2 开始,不再使用 node_modules 文件夹。相反,它会生成一个映射项目依赖关系的 .pnp.cjs 文件。这会导致更优化的依赖树和更快的项目启动和包安装;
  2. 零安装:此功能与 Plug’n’Play 结合使用,后者使用.pnp.cjs 文件来映射离线缓存中的包。这使您可以快速检索和安装已保存的软件包;
  3. 检查器:Yarn 带有一个内置的检查器,用于下载和安装包;

yarn在下载时使用了并行安装项目依赖,这在大型应用中的安装速度显著的快很多。

比如看一个简单的yarn4初始化的项目在这里插入图片描述
可以看到这个项目中安装了axios模块但没有使用node_module进行管理,而是使用了一个.pnp.cjs的文件进行管理

Yarn 和 NPM 的区别

YarnNPM
使用 yarn add 命令来安装依赖项使用 npm install 命令安装依赖项
使用并行安装依赖项按顺序安装依赖项
使用yarn.lock作为锁定文件使用package-lock.json作为锁定文件
支持即插即用功能,它会生成一个 .pnp.cjs 文件,其中包含项目的依赖关系图不支持
在安装大文件时更快安装大文件时更慢
支持零安装功能,允许您离线安装依赖项,几乎没有延迟不支持
在下载包时,它会利用包的许可信息在后台运行安全检查,以避免下载危险的脚本或导致依赖问题在 NPM 的早期版本中,安全性是一个主要问题。从版本 6 开始,每次安装包时,NPM 都会进行安全审计以避免漏洞并确保没有不兼容的依赖项
校验和验证包使用存储在 package-lock.json 文件中的 SHA-512 进行验证

Yarn 也支持 NPM 创建的 package-lock.json 文件,方便将版本数据从 NPM 迁移到 Yarn

yarn和npm3+的问题

在npm@3+ 和 yarn中,通过扁平化处理,解决依赖无法被共用,依赖层级太深的问题,所有的依赖都被平铺在node_modules中的一级目录。但是多个版本的包只有一个(最先安装的一个) 被提升上来,其余版本的包还是会嵌套安装到各自的依赖当中,之前提到的路径过长和重复安装的问题没有彻底解决。除此之外还有幽灵依赖的问题。比如本地没有显实的依赖axios这个模块,但是依赖了其他模块二其他模块依赖了axios这个模块,这就导致也会将axios模块安装到用户的node_module下,这导致用户在代码中实际也能使用axios模块的功能,显然这和我们的预期是不符合的。这就是幽灵依赖

Pnpm

pnpm,即performant npm,高性能的npm。相比起目前主流的包管理器,pnpm是速度快、节省磁盘空间的包管理器。

Pnpm性能对比

我们先看一下官网提供的性能对比数据:
在这里插入图片描述
在这里插入图片描述
可以看出,与目前主流的包管理器npm、yarn相比,无论有无cache、有无lockfile、有无node_modules,pnpm的安装速度都有明显的优势

Pnpm在磁盘中的存储

pnpm之所以这么快的最主要的原因是,pnpm通过hard link(硬连接)机制,节省磁盘空间并提升安装速度。
hard link(硬链接):多个文件名指向同一索引节点(Inode)。硬链接的作用 之一是允许一个文件拥有多个有效路径名,这样用户就可以建 立硬链接到重要的文件,以防止“误删”源数据

如果原始文件被删除,只要硬链接还存在,数据仍然可访问,文件并没有真正被删除。只有所有的硬链接都删除后,文件数据才会被清除

举个例子,如果有100各项目都依赖同一个三方模块,如果使用npm或者是yarn的话
那这个模块要被重复安装100次相同的“副本”。而使用 pnpm,package将被存放在一个统一的位置(如 Mac是 ~/.pnpm-store )。当安装软件包时,其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的package。因此,不会有上面提到的重复安装相同包的问题了。
当我们使用pnpm在本地一个项目安装axios时可以发现有个reused字段表之前已经安装过相同的包了。
而当安装版本不同的同名软件包时,仅会添加版本之间不同 的文件到存储器中,而不会因为一个文件的修改而保存package的所有文件。
同时该命令提供了一个选项,使用方法为pnpm store prune,它提供了一种用于删除一些不被全局项目所引用到的package的功能。如果需要,开发者可以用这个命令来管理已有的package。
在这里插入图片描述

Pnpm对node_Module的优化

除了节省磁盘空间,pnpm还有另一个重要特点是,建立非扁平的node_modules目录,并在引用依赖的时候通过sybolic link机制找到对应.pnpm目录的地址。我们看一下使用pnpm管理项目的node_module的目录结构
在这里插入图片描述
可以看到在node_module中都是平铺的形式管理三方依赖的模块,并且axioslodashnode_module中这两个文件夹的右侧都有一个小箭头,这是一个软连接实际是链接到了.pnpm下这样的路径:.pnpm中对应的模块。
sybolic link(软链接,也叫符号链接):类似于windows系统中的快捷方式,与硬链接不同,软链接就是一个 普通文件,只是数据块内容有点特殊,文件用户数据块中存放的内容是另一文件的路径名的指 向,通过这个方式可以快速定位到软连接所指向的源文件实体。
这样,pnpm 实现了相同模块不同版本之间隔离和复用。而且,node_modules目录是非扁平的,不存在由于提升带来的幽灵依赖问题。比如我们可以看在.pnpm目录下的axioslodash都有各自的node_module
在这里插入图片描述

小结

  1. pnpm 通过 hard link 在全局里面搞个 store 目录来存储 node_modules 依赖里面的 hard link 地址。这样节省了磁盘空间,并提升安装速度
  2. node_modules目录是非扁平的,在引用依赖的时候,则是通过 sybolic link 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。这样,避免了之前npm和yarn扁平/非扁平安装带来的一系列问题;

整个流程如下图所示:
在这里插入图片描述

对比

特性npmyarnpnpm
安装方式自动随 Node.js 安装需要单独安装需要单独安装
包缓存支持缓存,但缓存机制较为简单支持缓存,安装时从缓存中获取加速支持强大的磁盘缓存机制,避免重复下载和浪费空间
安装速度相对较慢,尤其在大量依赖时更快,尤其是有缓存时更快,采用硬链接方式减少重复依赖的存储
依赖管理使用 node_modules 存储依赖使用 node_modules 存储依赖,优化了依赖的管理方式使用硬链接和符号链接来创建依赖树,提高存储效率
工作空间(Workspaces)从 npm 7 开始支持工作空间原生支持工作空间,用于管理 monorepos支持工作空间,适用于 monorepos 项目
锁文件package-lock.jsonyarn.lockpnpm-lock.yaml
安装依赖的精确性有时依赖版本可能不一致(尤其在没有锁文件的情况下)确保锁文件的精确性,安装一致高精度的依赖版本控制,确保每个开发环境一致
并发下载支持并发下载,但效率较低支持并发下载,速度更快使用并发下载并且非常高效
兼容性与大多数现有工具兼容高度兼容 npm,支持较多工具兼容 npm 和 Yarn,支持多种工具
安装的存储优化存储空间相对浪费,所有模块都存储在 node_modules有一定优化,减少重复存储使用硬链接存储,节省磁盘空间,避免重复依赖存储
社区支持最大,几乎所有 JavaScript 项目都支持良好,尤其在 Facebook 和 JavaScript 社区中受欢迎较新,但支持增长迅速,逐渐得到许多大项目的青睐

Monorepo项目选择包管理器

Monorepo的思想是严格统一,多个项目放在一个仓库里面。比较知名的像Antd、babel、npm7都是在用Monorepo方式进行管理的。Monorepo常见目录结构如下

- packages- project1- src- index.ts- package.json- project2- src- index.ts- package.json- project3- src- index.ts- package.json
- package.json
- tsconfig.json

这样有以下几点好处:

  1. 统一工作流:由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况,对开发调试而言都带来了方便
  2. 降低基建成本:所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低
  3. 团队协作也更加容易 ,一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码,另一方面,git commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 commit,现在只需要提交一次,简化了 commit 记录,方便协作

当我们开发维护一个monorepo的项目时比较合适的包管理工具是pnpm,因为pnpm具有npmyarn没有的独特优势,具体有以下几点:
4. 支持 Workspaces:pnpm 原生支持 workspaces,可以自动管理多个子项目的依赖,确保所有的 package 依赖关系正确无误。

  1. 高效的存储机制:pnpm 采用硬链接和符号链接来存储依赖,而不是像 npm 或 yarn 那样直接复制 node_modules,从而极大地节省了磁盘空间。例如:
    如果多个 package 依赖相同的 react,pnpm 只会在全局存储中保存一份,而不同的 package 只会创建指向它的链接。
    这样比 npm 和 yarn 节省磁盘空间,并且加快安装速度。
  2. 自动处理 package 之间的依赖:pnpm 能够自动解析 monorepo 内部的依赖,例如 package-a 依赖 package-b,pnpm 可以直接链接,而不需要手动指定路径。
    避免了手动 npm link 或 yarn workspaces run 这样的复杂操作。
  3. 严格的依赖管理:pnpm 的 node_modules 采用严格模式,避免了 npm 和 yarn 可能导致的幽灵依赖问题(即 package.json 没有声明依赖但仍然可以使用)。

如何在 pnpm 中启用 Workspaces

需要使用pnpm管理多包项目非常简单。只需要按照下面步骤即可

创建 pnpm Monorepo

在根目录下创建 pnpm-workspace.yaml:

packages:- "packages/*"  # 这里会自动识别 packages 目录下的所有子项目

然后在 package.json 中启用 workspaces:

{"name": "my-monorepo","private": true,"devDependencies": {"pnpm": "^8.0.0"}
}
创建多个子项目

创建 packages/package-a/package.json:

{"name": "package-a","version": "1.0.0","main": "index.js","dependencies": {"lodash": "^4.17.21"}
}

创建 packages/package-b/package.json,并让 package-b 依赖 package-a:

{"name": "package-b","version": "1.0.0","main": "index.js","dependencies": {"package-a": "workspace:*"}
}
安装依赖
pnpm install

pnpm 会自动解析 workspace:* 依赖,确保 package-b 能够正确依赖 package-a。
依赖会被存储在 node_modules/.pnpm 下,并通过符号链接管理。

对比各包管理器在monorepo下的作用

特性pnpm workspacesyarn workspacesnpm workspaces
存储方式硬链接+符号链接(节省空间)直接安装在 node_modules直接安装在 node_modules
安装速度🚀 最快中等最慢
依赖隔离严格(无幽灵依赖)可能有幽灵依赖可能有幽灵依赖
支持 workspace:* 语法支持支持支持
自动解析内部依赖自动解析自动解析自动解析
适合 Monorepo非常适合适合适合
缓存机制强大的缓存机制,避免重复安装依赖默认缓存,效率较高默认缓存,效率较低
依赖版本控制高精度版本控制,严格管理依赖高精度版本控制高精度版本控制
硬链接优化高效硬链接管理❌ 不支持❌ 不支持

http://www.ppmy.cn/ops/158584.html

相关文章

微信小程序地图标记点,安卓手机一次性渲染不出来的问题

问题描述: 如果微信小程序端,渲染的标记物太多,安卓手机存在标记物不显示的问题,原因初步判断是地图还没有渲染完,标记物数据已经加载完了,导致没有在地图上显示。 解决办法: 使用map组件的b…

DeepSeek4j 已开源,支持思维链,自定义参数,Spring Boot Starter 轻松集成,快速入门!建议收藏

DeepSeek4j Spring Boot Starter 快速入门 简介 DeepSeek4j 是一个专为 Spring Boot 设计的 AI 能力集成启动器,可快速接入 DeepSeek 大模型服务。通过简洁的配置和易用的 API,开发者可轻松实现对话交互功能。 环境要求 JDK 8Spring Boot 2.7Maven/Gr…

LeetCode《算法通关手册》 1.2 数组排序

Python强推:算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn) 目录 文章目录 1.2 数组排序1.2.1 选择排序1.2.2 冒泡排序[283. 移动零 - 力扣(LeetCode)](https://leetcode.cn/p…

【网络安全 | 漏洞挖掘】跨子域账户合并导致的账户劫持与删除

未经许可,不得转载。 文章目录 概述正文漏洞成因概述 在对目标系统进行安全测试时,发现其运行着两个独立的域名——一个用于司机用户,一个用于开发者/企业用户。表面上看,这两个域名各自独立管理账户,但测试表明它们在处理电子邮件变更时存在严重的逻辑漏洞。该漏洞允许攻…

Xilinx kintex-7系列 FPGA支持PCIe 3.0 吗?

Xilinx kintex-7系列资源如下图 Xilinx各系列的GT资源类型和性能 PCIe Gen1/2/3的传输速率对比 K7上面使用的高速收发器GTX最高速率为12.5GT/s, PCIe Gen2 每个通道的传输速率为 5 GT/s。 PCIe Gen3 每个通道的传输速率为 8 GT/s。 所以理论上硬件支持PCIe3.0&#…

Python基于 Flask 创建简单Web服务并接收文件

在全部网口上创建web服务, 监听8080端口关闭debug模式GET时返回HTML界面, 用于提交文件POST到 /upload 时, 从接收的 file 变量中读取文件, 并传递给 opencv 解析为 image 对象 from flask import Flask, request, redirect, url_for import os import cv2 import numpy impor…

【LINUX】常用指令查询

目录 文件操作查看目录(ls/ll)进入目录(cd)创建目录(mkdir)移动操作(mv)删除操作(rm) 文件操作 查看目录(ls/ll) 查看当前目录下的所…

【Qt】:概述(下载安装、认识 QT Creator)

🌈 个人主页:Zfox_ 🔥 系列专栏:Qt 目录 一:🔥 介绍 🦋 什么是 QT🦋 QT 发展史🦋 Qt版本🦋 QT 优点 一:🔥 搭建Qt开发环境 &#x1f9…