JavaScript包管理器比较:npm, Yarn,还是pnpm?

包管理器三个主要工具:

实际上,我们在所有包管理器之间已经实现了功能上的平衡,因此您很可能会根据非功能性的需求(例如安装速度、存储消耗或与现有工作流程的契合程度)来决定使用哪个包管理器。

当然,您选择如何使用每个包管理器可能会有所不同,但它们都包含主要功能。您可以使用任何这些包管理器执行以下操作:

  • 处理和编写元数据
  • 批量安装或更新所有依赖项
  • 添加、更新和删除依赖项
  • 运行脚本
  • 发布软件包
  • 执行安全审查

尽管存在这种平衡,但包管理器在内部有所不同。传统上,npmYarn会将依赖项安装在平铺的node_modules文件夹中。但是这种依赖关系解析策略并不完美。

因此,pnpm引入了一些新概念,以更有效地在嵌套的node_modules文件夹中存储依赖项。Yarn Berry更进一步,通过摒弃node_modules并采用其Plug’n’Play(PnP)模式。

在本文中,我们将根据一系列标准比较这些包管理器,包括安装工作流程、项目结构、配置文件等。

如何使用配套项目

我创建了一个配套的React应用程序,以演示不同包管理器的独特概念。每个包管理器变体都有相应的Git分支。这也是我用来创建本文后面部分中的性能表的项目。

尽管应用程序类型对本文主题并不重要,但我选择了一个中等大小和现实的项目,以便能够阐明不同方面;作为最近的一个例子,Yarn BerryPnP机制引起了一些关于兼容性问题的激烈讨论,而这个项目适合帮助进行检查。

JavaScript包管理器的简要历史

有史以来发布的第一个包管理器是npm,发布于2010年1月。它奠定了当今包管理器运作的核心原则。

如果npm已经存在了超过10年,为什么还会有任何替代方案呢?以下是一些重要原因:

  • 使用不同的依赖关系解析算法,具有不同的node_modules文件夹结构(嵌套 vs. 平坦,node_modules vs. PnP模式)
  • Hoisting支持的不同,这对安全性有影响
  • 不同的锁定文件格式,每种格式都有性能影响
  • 不同的存储包的磁盘方法,这对磁盘空间利用效率有影响
  • 不同对多包项目(即工作空间)的支持程度,这影响了大型monorepos的可维护性和速度
  • 不同对新工具和命令的需求,每种工具和命令都对开发体验有影响
  • 相关地,不同对通过插件和社区工具进行扩展性的需求

让我们深入了解一下,这些需求是如何在npm崛起之后被识别出来的,Yarn Classic如何解决了其中一些问题,pnpm如何在这些概念上进行了扩展,以及作为Yarn Classic继任者的Yarn Berry如何试图打破这些传统概念和流程。

先行者npm

npm是包管理器的鼻祖。错误地,许多人认为npm“Node package manager”的缩写,但事实并非如此。尽管如此,它仍然与Node.js运行时捆绑在一起。

它的发布构成了一场革命,因为在那之前,项目依赖关系是手动下载和管理的。像package.json文件及其元数据字段(例如devDependencies)、将依赖项存储在node_modules中、自定义脚本、公共和私有包注册表等概念都是由npm引入的。

2020年,GitHub收购了npm,因此从原则上讲,npm现在由Microsoft管理。在撰写本文时,最新的主要版本是v8,于2021年10月发布。

Yarn(v1 / Classic),带来了许多创新

在2016年10月的一篇博客文章中,Facebook宣布与Google和其他几家公司合作,开发一个新的包管理器,旨在解决当时npm存在的一致性、安全性和性能问题。他们将这个替代方案命名为Yarn,即Yet Another Resource Negotiator的缩写。

尽管他们将Yarn的架构设计基于npm确立的许多概念和流程,但Yarn在初始发布时对包管理器的景观产生了重大影响。与npm不同,Yarn并行化操作以加快安装过程的速度,这是npm早期版本的一个主要痛点。

Yarn提高了DX、安全性和性能的标准,还发明了许多概念,包括:

  • 本机monorepo支持
  • 缓存感知安装
  • 离线缓存
  • 锁文件

Yarn v1于2020年进入维护模式。自那时起,v1.x系列被视为遗留版本,并更名为Yarn Classic。其继任者Yarn v2Berry现在是活跃的开发分支。

pnpm,快速且节省磁盘空间

pnpm的第一个版本于2017年由Zoltan Kochan发布。它是npm的一个可替代选择,因此如果您有一个npm项目,您可以立即使用pnpm

pnpm的创建者与npmYarn的主要问题是,跨项目使用的依赖关系的重复存储。尽管Yarn Classic在性能上优于npm,但它使用了与其他人相同的依赖关系解析方法,这对pnpm的创建者来说是不可接受的:npmYarn Classic使用提升来平铺他们的node_modules

与提升不同,pnpm引入了一种替代的依赖关系解析策略:内容可寻址存储。该方法导致了一个嵌套的node_modules文件夹,它在您的主目录(~/.pnpm-store/)中的全局存储中存储软件包。每个依赖项的每个版本在该文件夹中只存储一次,构成了一个单一的真相来源,并节省了相当多的磁盘空间。

通过一个node_modules布局实现了这一点,使用符号链接来创建一个依赖项的嵌套结构,在文件夹中每个软件包的每个文件都是一个到存储的硬链接。官方文档的下面这张图解释了这一点。

pnpm的影响可以从他们的2021年报告中看出:竞争对手想要采用pnpm的安装概念,如符号链接的node_modules结构和由于内容可寻址存储的创新而节省磁盘空间的管理。

Yarn(v2,Berry),用Plug’n’Play重新发明轮子

Yarn 2于2020年1月发布,被标榜为原始Yarn的重大升级。Yarn团队开始将其称为Yarn Berry,以使其更明显地表明它实际上是一个具有新代码库和新原则的新包管理器。

Yarn Berry的主要创新是其Plug’n’Play(PnP)方法,这是一种解决node_modules的策略。与生成node_modules不同,它生成了一个包含依赖项查找表的.pnp.cjs文件,这样可以更高效地处理,因为它是一个单个文件,而不是一个嵌套的文件夹结构。此外,每个软件包都存储为.zip文件,存储在.yarn/cache/文件夹中,占用的磁盘空间比node_modules文件夹少。

所有这些变化,而且速度很快,在发布后引起了很多争议。PnP的重大变化要求维护者更新其现有软件包,以使其与之兼容。全新的PnP方法被默认使用,并且最初没有简单地回退到node_modules的方法,这导致许多著名开发人员公开批评Yarn 2没有使其成为可选择的。

Yarn Berry团队在随后的发布中解决了许多问题。为了解决PnP的不兼容性问题,团队提供了一些简化默认操作模式的方法。通过一个node_modules插件的帮助,只需要一行配置即可使用传统的node_modules方法。

此外,随着时间的推移,JavaScript生态系统对PnP提供了越来越多的支持,您可以在此兼容性表中看到,一些大型项目已经开始采用Yarn Berry。在我的配套项目中,我还能够正确地实现PnP与我的演示React项目。

尽管Yarn Berry还很年轻,但它也已经在包管理器领域产生了影响 — pnpm在2020年底采用了PnP方法。

安装工作流程

首先,需要在每个开发人员的本地和CI/CD系统上安装包管理器。

npm

npmNode.js一起发布,因此不需要额外的步骤。除了下载适用于您的操作系统的Node.js安装程序之外,使用CLI工具来管理软件版本已经成为常见做法。在Node的上下文中,Node Version Manager(nvm)Volta已经成为非常方便的工具。

Yarn ClassicYarn Berry

可以以不同的方式安装Yarn 1,例如,使用$ npm i -g yarn作为npm包安装。

Yarn Classic迁移到Yarn Berry的推荐方式是:

  • 安装或更新Yarn Classic到最新的1.x版本
  • 使用yarn set version命令升级到最新的现代版本:$ yarn set version berry

然而,安装Yarn Berry的推荐方式是通过Corepack
Corepack是由Yarn Berry的开发人员创建的。该倡议最初命名为package manager manager(pmm),并与NodeLTS v16中合并。

通过Corepack的帮助,您不必“单独”安装npm的替代包管理器,因为Node包括了Yarn Classic、Yarn Berrypnpm二进制文件作为垫片。这些垫片允许用户在首次安装它们而不必显式安装它们,并且不会混淆Node发行版。

CorepackNode.js ≥ v16.9.0预安装。但是,对于旧的Node版本,您可以使用$ npm install -g corepack来安装它。

在使用之前,先启用Corepack。下面的示例显示了如何在Yarn Berry v3.1.1中激活它。

您需要先选择

1
$ corepack enable

安装了垫片但需要激活具体版本

1
$ corepack prepare yarn@3.1.1 --activate
pnpm

您可以使用$ npm i -g pnpm安装pnpm作为npm包。您也可以使用Corepack安装pnpm

1
$ corepack prepare pnpm@6.24.2 --activate

项目结构

在本节中,您将一目了然地看到不同包管理器的主要特征。您可以轻松地发现哪些文件用于配置特定的包管理器,以及哪些文件是由安装步骤生成的。

所有包管理器都将所有重要的元信息存储在项目清单文件package.json中。此外,根级别的配置文件可以用于设置私有注册表或依赖项解析方法。

通过安装步骤,依赖关系存储在文件结构中(例如,位于node_modules中),并生成一个锁定文件。此部分不考虑工作区设置,因此所有示例仅显示一个位置存储依赖项。

npm

使用$ npm install,或更短的$ npm i,将生成一个package-lock.json文件和一个node_modules文件夹。可以将一个可选的.npmrc配置文件放置在根级别。有关锁定文件的更多信息,请参阅下一节。

1
2
3
4
├── node_modules/
├── .npmrc
├── package-lock.json
└── package.json
Yarn Classic

运行$ yarn将创建一个yarn.lock文件和一个node_modules文件夹。.yarnrc文件也可以是一个配置选项;Yarn Classic也支持.npmrc文件。可以选择使用缓存文件夹(.yarn/cache/)和一个存储当前Yarn Classic版本的位置(.yarn/releases/)。不同的配置方式可以在比较配置部分中看到。

1
2
3
4
5
6
7
8
9
.
├── .yarn/
│ ├── cache/
│ └── releases/
│ └── yarn-1.22.17.cjs
├── node_modules/
├── .yarnrc
├── package.json
└── yarn.lock
Yarn Berry(使用node_modules

无论安装模式如何,您在Yarn Berry项目中都将处理更多的文件和文件夹,而不是使用其他包管理器的项目。一些是可选的,一些是强制性的。

Yarn Berry不再支持.npmrc.yarnrc文件;相反,需要一个.yarnrc.yml配置文件。对于生成的node_modules文件夹的传统工作流程,您必须提供一个nodeLinker配置,该配置使用node_modules或受pnpm启发的安装变体。

1
2
# .yarnrc.yml
nodeLinker: node-modules # 或 pnpm

运行$ yarn将所有依赖项安装到一个node_modules文件夹中。将生成一个yarn.lock文件,这是更新的,但与Yarn Classic不兼容。此外,生成一个.yarn/cache/文件夹用于离线安装。releases文件夹是可选的,并存储项目使用的Yarn Berry版本,如我们将在比较配置部分中看到的那样。

1
2
3
4
5
6
7
8
9
.
├── .yarn/
│ ├── cache/
│ └── releases/
│ └── yarn-3.1.1.cjs
├── node_modules/
├── .yarnrc.yml
├── package.json
└── yarn.lock
Yarn Berry(使用PnP

对于严格和松散的PnP模式,执行$ yarn将生成.yarn/cache/.yarn/unplugged/,以及.pnp.cjsyarn.lock文件。PnP strict是默认模式,但对于松散模式,需要一个配置。

1
2
3
# .yarnrc.yml
nodeLinker: pnp
pnpMode: loose

PnP项目中,.yarn/文件夹很可能包含一个sdk/文件夹,以提供IDE支持,以及一个releases/文件夹。根据您的用例,.yarn/中可能还包含更多的文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
.
├── .yarn/
│ ├── cache/
│ ├── releases/
│ │ └── yarn-3.1.1.cjs
│ ├── sdk/
│ └── unplugged/
├── .pnp.cjs
├── .pnp.loader.mjs
├── .yarnrc.yml
├── package.json
└── yarn.lock
pnpm

一个pnpm项目的初始状态看起来与npmYarn Classic项目一样 - 您需要一个package.json文件。通过$ pnpm i安装依赖项后,将生成一个node_modules文件夹,但其结构完全不同,因为它采用了内容可寻址的存储方法。

pnpm还生成自己版本的锁定文件pnp-lock.yml。您可以通过可选的.npmrc文件提供额外的配置。

1
2
3
4
5
├── node_modules/
│ └── .pnpm/
├── .npmrc
├── package.json
└── pnpm-lock.yml

lock文件和依赖项存储

如前所述,每个包管理器都会创建锁定文件。

锁定文件精确地存储了为您的项目安装的每个依赖项的版本,从而实现更可预测和确定性的安装。这是必需的,因为依赖项版本很可能是以版本范围(例如,≥ v1.2.5)声明的,因此,如果您不“锁定”您的版本,实际安装的版本可能会有所不同。

锁定文件有时还会存储校验和,我们将在安全性部分更深入地介绍。

锁定文件自npm v5(package-lock.json)pnpm从第一天(pnpm-lock.yaml)开始使用,并在Yarn Berry中以新的YAML格式(yarn.lock)`出现。

在前一节中,我们看到了传统的方法,即将依赖项安装在node_modules文件夹结构中。这是npm、Yarn Classicpnpm都采用的方案,其中pnpm比其他方案更高效。

PnP模式下的Yarn Berry做法不同。依赖项不是存储在node_modules文件夹中,而是作为zip文件存储在.yarn/cache/.pnp.cjs文件的组合中。

最好将这些锁定文件纳入版本控制,因为它解决了“在我的机器上可以工作”的问题——每个团队成员都安装相同的版本。

CLI命令

下表比较了npm、Yarn Classic、Yarn Berrypnpm中的一组不同CLI命令。这绝不是一个完整的列表,但构成了一个速查表。本节不涵盖与工作区相关的命令。

npmpnpm特别提供了许多命令和选项的别名,这意味着命令可以有不同的名称,例如,$ npm install$ npm add相同。此外,许多命令选项都有简短的版本,例如,-D代替--save-dev
在表格中,我将所有简短版本称为别名。使用所有包管理器,您可以通过用空格分隔它们来添加、更新或删除多个依赖项(例如,npm update react react-dom)。为了清晰起见,示例仅显示了单个依赖项的用法。

依赖管理

该表格涵盖了用于安装或更新package.json中指定的所有依赖项,或通过在命令中指定它们来安装或更新多个依赖项的依赖项管理命令。

Action npm Yarn Classic Yarn Berry pnpm
install deps in package.json npm install alias: iadd yarn install or yarn like Classic pnpm install alias: i
update deps in package.json acc. semver npm update alias: upupgrade yarn upgrade yarn semver up (via plugin) pnpm update alias: up
update deps in package.json to latest N/A yarn upgrade --latest yarn up pnpm update --latest alias: -L
update deps acc. semver npm update react yarn upgrade react yarn semver up react pnpm up react
update deps to latest npm update react@latest yarn upgrade react --latest yarn up react pnpm up -L react
update deps interactively N/A yarn upgrade-interactive yarn upgrade-interactive (via plugin) $ pnpm up --interactive alias: -i
add runtime deps npm i react yarn add react like Classic pnpm add react
add dev deps npm i -D babel alias: --save-dev yarn add -D babel alias:  --dev like Classic pnpm add -D babel alias: --save-dev
add deps to package.json without semver npm i -E react alias: --save-exact yarn add -E react alias: --exact like Classic pnpm add -E react alias: --save-exact
uninstall deps and remove from package.json npm uninstall react alias: removermrununlink yarn remove react like Classic pnpm remove react alias: rmununinstall
uninstall deps w/o update of package.json npm uninstall  --no-save N/A N/A N/A

Package 指令

以下示例展示了如何在开发过程中管理构成实用工具的Package — 即二进制文件,例如 ntl,用于交互式地执行脚本。表格中使用的术语如下:

  • Package:依赖项或二进制文件
  • Binary:从 node_modules/.bin/ 或 .yarn/cache/(PnP)中执行的可执行实用程序

重要的是要理解,出于安全原因,Yarn Berry 只允许我们执行在 package.json 中指定的二进制文件,或者在您的 bin 元字段中公开的二进制文件。pnpm 具有相同的安全行为。
| Action | npm | Yarn Classic | Yarn Berry | pnpm |
| — | — | — | — | — |
| install packages globally | npm i -g ntl alias: --global | yarn global add ntl | N/A (global removed) | pnpm add --global ntl |
| update packages globally | npm update -g ntl | yarn global upgrade ntl | N/A | pnpm update --global ntl |
| remove packages globally | npm uninstall -g ntl | yarn global remove ntl | N/A | pnpm remove --global ntl |
| run binaries from terminal | npm exec ntl | yarn ntl | yarn ntl | pnpm ntl |
| run binaries from script | ntl | ntl | ntl | ntl |
| dynamic package execution | npx ntl | N/A | yarn dlx ntl | pnpm dlx ntl |
| add runtime deps | npm i react | yarn add react | like Classic | pnpm add react |
| add dev deps | npm i -D babel alias: --save-dev | yarn add -D babel alias: --dev | like Classic | pnpm add -D babel alias: --save-dev |
| add deps to package.json without semver | npm i -E react alias: --save-exact | yarn add -E react alias: --exact | like Classic | pnpm add -E react alias: --save-exact |
| uninstall deps and remove from package.json | npm uninstall react alias: removermrununlink | yarn remove react | like Classic | pnpm remove react alias: rmununinstall |
| uninstall deps w/o update of package.json | npm uninstall --no-save | N/A | N/A | N/A |

配置文件

配置包管理器的操作既可以在您的 package.json 中进行,也可以在专用的配置文件中进行。以下是一些配置选项的示例:

  • 定义要使用的确切版本
  • 使用特定的依赖项解析策略
  • 配置访问私有注册表
  • 告诉包管理器在 monorepo 中的工作区所在位置
npm

大多数配置都在专用的配置文件(.npmrc)中进行。

如果您想要使用 npm 的工作区功能,则必须通过使用工作区元数据字段向 package.json 中添加配置,以告诉 npm 在哪里找到构成子项目或工作区的文件夹。

1
2
3
4
5
6
7
{
// ...
"workspaces": [
"hooks",
"utils"
]
}

每个包管理器都可以直接使用公共 npm 注册表。在公司环境中使用共享库时,您很可能希望在不将其发布到公共注册表的情况下重用它们。要配置私有注册表,可以在.npmrc文件中执行此操作。

1
2
# .npmrc
@doppelmutzi:registry=https://gitlab.doppelmutzi.com/api/v4/projects/41/packages/npm/

npm 有许多配置选项,并且最好在文档中查看它们。

Yarn Classic

您可以在 package.json 中设置 Yarn 工作区。它类似于 npm,但工作区必须是私有包。

1
2
3
4
5
{
// ...
"private": true,
"workspaces": ["workspace-a", "workspace-b"]
}

任何可选配置都放在.yarnrc文件中。常见的配置选项是设置 yarn-path,它强制每个团队成员使用特定的二进制版本。yarn-path 指向一个包含特定 Yarn 版本的文件夹(例如 .yarn/releases/)。您可以使用yarn policies命令安装 Yarn Classic 版本。

Yarn Berry Yarn Berry中配置工作区与 Yarn Classic 中的配置方式类似,都是使用 package.json。大多数Yarn Berry配置都在.yarnrc.yml中进行,有许多配置选项可用。Yarn Classic 的示例也是可能的,但元数据字段被重命名为 yarnPath

1
2
# .yarnrc.yml
yarnPath: .yarn/releases/yarn-3.1.1.cjs

Yarn Berry 可以通过使用 yarn plugin import 扩展插件。此命令会更新 .yarnrc.yml

1
2
3
4
# .yarnrc.yml
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-semver-up.cjs
spec: "https://raw.githubusercontent.com/tophat/yarn-plugin-semver-up/master/bundles/%40yarnpkg/plugin-semver-up.js"

正如在历史部分中所描述的,由于不兼容性,PnP 严格模式中的依赖关系可能会出现问题。对于这种 PnP 问题,有一个典型的解决方案:packageExtensions 配置属性。您可以按照下面的示例执行相应的项目。

1
2
3
4
5
# .yarnrc.yml
packageExtensions:
"styled-components@*":
dependencies:
react-is: "*"

pnpm pnpm 使用与npm相同的配置机制,因此可以使用.npmrc文件。配置私有注册表的方法也与 npm 相同。

使用 pnpm 的工作区功能,可以支持多包项目。要初始化 monorepo,必须在pnpm-workspace.yaml文件中指定包的位置。

1
2
3
# pnpm-workspace.yaml
packages:
- 'packages/**'

Monorepo 支持

什么是 monorepoMonorepo 是一个存放多个项目的存储库,这些项目被称为工作区或包。它是一种项目组织策略,将所有内容放在一个地方,而不是使用多个存储库。

当然,这会带来额外的复杂性。Yarn Classic 是第一个启用此功能的包管理器,但现在每个主要的包管理器都提供了工作区功能。本节展示了如何使用不同的包管理器配置工作区。

npm

npm团队在 v7 中发布了备受期待的 npm 工作区功能。它包含了许多 CLI 命令,可以帮助从根包内部管理多包项目。大多数命令可以与工作区相关的选项一起使用,以告诉 npm 是否应该针对特定、多个或所有工作区运行。

1
2
3
4
5
6
7
8
9
10
# 安装所有工作区的所有依赖项
$ npm i --workspaces.
# 只针对一个包运行
$ npm run test --workspace=hooks
# 针对多个包运行
$ npm run test --workspace=hooks --workspace=utils
# 针对所有运行
$ npm run test --workspaces
# 忽略所有缺少测试的包
$ npm run test --workspaces --if-present

与其他包管理器不同,npm v8 目前不支持高级过滤或并行执行多个与工作区相关的命令。

Yarn Classic

2017 年 8 月,Yarn 团队宣布了一流的monorepo支持,即工作区功能。在此之前,只能使用第三方软件(如 Lerna)在多包项目中使用包管理器。这个功能的添加为其他包管理器实现了这样一个功能铺平了道路。

1
2
3
4
5
6
7
8
9
10
# 安装所有工作区的所有依赖项
$ yarn
# 显示依赖树
$ yarn workspaces info
# 仅为一个包运行启动命令
$ yarn workspace awesome-package start
# 将 Webpack 添加到包中
$ yarn workspace awesome-package add -D webpack
# 将 React 添加到所有包中
$ yarn add react -W Yarn
Berry Yarn

Berry 从一开始就具有工作区功能,因为其实现是建立在 Yarn Classic 的概念之上的。在 Reddit 评论中,Yarn Berry 的主要开发人员对工作区导向功能(包括以下内容)进行了简要概述:

  • $ yarn add --interactive:在安装包时,可以重用其他工作区中的版本
  • $ yarn up:更新所有工作区的包
  • $ yarn workspaces focus:仅为单个工作区安装依赖项
  • $ yarn workspaces foreach:在所有工作区上运行命令
    Yarn Berry 大量使用协议,这些协议可以在 package.json 文件的 dependenciesdevDependencies 字段中使用。其中之一是 workspace: protocol.

Yarn Classic 的工作区不同,Yarn Berry 明确规定依赖项必须是此 monorepo 中的包之一。否则,如果版本不匹配,Yarn Berry 可能会尝试从远程注册表中获取版本。

1
2
3
4
5
6
7
8
{
// ...
"dependencies": {
"@doppelmutzi/hooks": "workspace:*",
"http-server": "14.0.0",
// ...
}
}
pnpm

借助其 workspace: protocolpnpm 类似于 Yarn Berry 一样便于处理 monorepo 项目。许多 pnpm 命令接受选项,如 --recursive (-r) --filter,在 monorepo 上下文中特别有用。其原生过滤命令也是 Lerna 的一个很好的补充或替代品。

1
2
3
4
# 清理所有工作区
pnpm -r exec -- rm -rf node_modules && rm pnpm-lock.yaml
# 运行 @doppelmutzi 下所有工作区的所有测试
pnpm recursive run test --filter @doppelmutzi/

性能和磁盘空间效率

性能是决策的重要部分。本节根据一个小型项目和一个中等大小项目进行基准测试。以下是有关样本项目的一些说明:

两组基准测试均不使用工作区功能 小项目指定了 33 个依赖项 中型项目指定了 44 个依赖项 我为三种用例(UC)进行了测量,分别针对每个包管理器变体。要了解详细评估和解释,请查看项目 1 (P1) 和项目2 (P2)的结果。

  • UC 1:无缓存/存储,无锁文件,无 node_modules.pnp.cjs
  • UC 2:存在缓存/存储,无锁文件,无 node_modules.pnp.cjs
  • UC 3:存在缓存/存储,存在锁文件,无 node_modules.pnp.cjs
    使用工具 gnomon 来测量安装所需的时间(例如,$ yarn | gnomon)。此外,还测量了生成文件的大小,例如,$ du -sh node_modules

根据我的项目和测量结果,Yarn Berry PnP 严格模式在所有用例和两个项目中的安装速度方面都是赢家。

安全功能

npm

在处理有问题软件包时,npm 有时候过于宽容,并且曾经遇到一些直接影响许多项目的安全漏洞。例如,在版本 5.7.0 中,当您在 Linux 操作系统上执行 sudo npm 命令时,可能会改变系统文件的所有权,导致操作系统无法使用。

另一起事件发生在 2018 年,涉及比特币的盗窃。基本上,流行的 Node.js 软件包EventStream在其版本 3.3.6 中添加了一个恶意依赖项。这个恶意软件包包含一个加密的负载,试图从开发人员的计算机中窃取比特币。

为了帮助解决这些问题,较新的 npm 版本使用 SHA-512 加密算法在 package-lock.json 中检查您安装的软件包的完整性。

总的来说,npm 已经在尽力弥补他们的安全漏洞,尤其是与 Yarn 相比更为明显的漏洞。

Yarn

Yarn ClassicYarn Berry 起,它们就从一开始就通过在 yarn.lock 中存储校验和来验证每个软件包的完整性。Yarn 还试图阻止您在安装过程中检索未在package.json中声明的恶意软件包:如果发现不匹配,则会中止安装

PnP 模式的 Yarn Berry 不会遭受传统 node_modules 方法的安全问题。与 Yarn Classic 相比,Yarn Berry 改善了命令执行的安全性。您只能执行在 package.json 中显式声明的依赖项的二进制文件。这个安全功能与 pnpm 类似,我接下来会描述。

pnpm

pnpm 也使用校验和在执行每个已安装软件包的代码之前验证其完整性。

正如我们上面提到的,由于 hoistingnpmYarn Classic 都存在安全问题。pnpm 避免了这个问题,因为它的模型不使用 hoisting;相反,它生成嵌套的 node_modules 文件夹,消除了非法依赖访问的风险。这意味着只有在package.json中显式声明的依赖项才能访问其他依赖项。

这在monorepo设置中尤为重要,因为我们讨论过,hoisting 算法有时会导致虚假依赖项和重复。

开源项目的采用

这些活跃维护的开源项目现在使用哪些包管理器,这可能会在选择包管理器时给您提供参考。
| npm | Yarn Classic | Yarn Berry | pnpm |
| — | — | — | — |
| Svelte | React | Jest (with node_modules) | Vue 3 |
| Preact | Angular | Storybook (with node_modules) | Browserlist |
| Express.js | Ember | Babel (with node_modules) | Prisma |
| Meteor | Next.js | Redux Toolkit (with node_modules) | SvelteKit |
| Apollo Server | Gatsby | | |
| | Nuxt | | |
| | Create React App | | |
| | webpack-cli | | |
| | Emotion | |

有趣的是,这些开源项目中没有一个使用 PnP 方法。

结论

我们在所有主要的包管理器之间几乎实现了功能平等。但是,它们在内部仍有很大的不同。

pnpm 乍一看像是 npm,因为它们的 CLI 使用方式类似,但是管理依赖关系有很大不同;pnpm 的方法可以提高性能并实现最佳的磁盘空间效率。Yarn Classic 仍然非常流行,但被视为遗留软件,并且支持可能会在不久的将来被停止。Yarn Berry PnP 是新晋的宠儿,但尚未充分发挥其再次革命化包管理器领域的潜力。

本文的目标是为您提供多种视角,以便自行决定使用哪个包管理器。不推荐特定的包管理器。这取决于您如何权衡不同的要求 —— 所以您仍然可以选择自己喜欢的!