pnpm:快速、节省磁盘空间的包管理器

pnpm:快速、节省磁盘空间的包管理器

在现代 JavaScript 开发中,包管理器是不可或缺的工具。它们负责下载、安装、更新和管理项目所需的依赖项。长期以来,npm 和 Yarn 一直是主流的选择。然而,一个名为 pnpm 的后起之秀正逐渐崭露头角,它以其独特的依赖管理方式,提供了更快的安装速度和更节省磁盘空间的优势。本文将深入探讨 pnpm 的工作原理、优势、使用方法,以及与 npm 和 Yarn 的对比,帮助您全面了解这个强大的包管理器。

1. 传统包管理器的困境:node_modules 的“黑洞”

在深入了解 pnpm 之前,我们需要先了解传统包管理器(如 npm 和 Yarn)在依赖管理方面存在的问题。

1.1. 依赖的重复安装

假设您有多个项目,每个项目都依赖于同一个库,例如 lodash。使用 npm 或 Yarn 时,每个项目都会在自己的 node_modules 文件夹中安装一份 lodash 的副本。这意味着,如果您有 10 个项目都依赖 lodash,那么您的硬盘上就会有 10 份 lodash 的拷贝。这不仅浪费了大量的磁盘空间,也增加了安装时间。

1.2. 扁平化依赖的隐患(npm v3 之前)

在 npm v3 之前,npm 采用的是嵌套的依赖结构。这意味着如果 A 依赖 B,B 依赖 C,那么 node_modules 的结构会是这样的:

node_modules/
A/
node_modules/
B/
node_modules/
C/

这种嵌套结构在某些情况下会导致路径过长的问题,尤其是在 Windows 系统上。此外,如果多个依赖项依赖于同一个库的不同版本,可能会导致版本冲突和难以调试的问题。

1.3. 幽灵依赖(Phantom Dependencies)

npm v3 之后,为了解决嵌套依赖的问题,npm 采用了扁平化的依赖结构。它会将所有依赖项尽可能地提升到顶层的 node_modules 文件夹中。这虽然解决了路径过长的问题,但也引入了另一个问题:幽灵依赖。

幽灵依赖指的是,您的项目可以访问到那些并没有在 package.json 中明确声明的依赖项。这是因为这些依赖项可能是您声明的某个依赖项的子依赖,而被提升到了顶层的 node_modules 文件夹中。

幽灵依赖的问题在于,它破坏了依赖关系的明确性。如果您的项目在不知不觉中依赖了某个幽灵依赖,而未来某个时候这个幽灵依赖被移除(例如,因为您升级了某个依赖项,而这个依赖项不再依赖这个幽灵依赖),您的项目可能会崩溃。

2. pnpm 的核心理念:内容寻址存储和硬链接

pnpm 采用了一种完全不同的依赖管理方式,解决了传统包管理器存在的诸多问题。它的核心理念是:

  • 内容寻址存储(Content-addressable Store)

    • pnpm 不会将每个依赖项的副本都安装到项目的 node_modules 文件夹中。相反,它会在全局维护一个存储库(通常位于用户主目录下的 .pnpm-store 文件夹中)。
    • 当您安装一个依赖项时,pnpm 会根据该依赖项的内容计算出一个唯一的哈希值。
    • 如果这个哈希值对应的依赖项已经存在于全局存储库中,pnpm 就不会再次下载。
    • 如果不存在,pnpm 会下载该依赖项,并将其存储到全局存储库中。
  • 硬链接(Hard Links)和符号链接(Symbolic Links)

    • 在项目的 node_modules 文件夹中,pnpm 不会直接复制依赖项的文件。
    • 相反,它会创建硬链接或符号链接指向全局存储库中的实际文件。
    • 硬链接:指向文件系统中同一个 inode(索引节点)的多个文件路径。它们共享相同的文件内容,但可以有不同的文件名。删除其中一个硬链接不会影响其他硬链接或原始文件。
    • 符号链接(软链接):类似于 Windows 中的快捷方式或 macOS 中的别名。它是一个特殊的文件,其中包含指向另一个文件或目录的路径。

2.1. pnpm 的依赖安装过程

下面通过一个例子来详细说明 pnpm 的依赖安装过程:

  1. 假设您有一个项目,需要安装 lodash
  2. pnpm 首先会检查 package.json 文件,确定需要安装 lodash 的哪个版本。
  3. 然后,pnpm 会根据 lodash 的内容计算出一个哈希值。
  4. pnpm 会检查全局存储库(.pnpm-store)中是否已经存在这个哈希值对应的 lodash 版本。
    • 如果存在,pnpm 会直接进行下一步。
    • 如果不存在,pnpm 会从 npm 仓库下载 lodash,并将其存储到全局存储库中。
  5. 接下来,pnpm 会在项目的 node_modules 文件夹中创建一个名为 .pnpm 的隐藏文件夹。
  6. .pnpm 文件夹中,pnpm 会创建一个以 lodash 版本号和哈希值命名的文件夹(例如,[email protected]_somehash)。
  7. 在这个文件夹中,pnpm 会创建硬链接或符号链接,指向全局存储库中 lodash 的实际文件。
  8. 最后,pnpm 会在项目的 node_modules 文件夹中创建一个名为 lodash 的符号链接,指向 .pnpm 文件夹中的 lodash 文件夹。

通过这种方式,pnpm 实现了以下效果:

  • 节省磁盘空间:由于所有项目都共享全局存储库中的依赖项,因此避免了重复安装,大大节省了磁盘空间。
  • 加速安装:由于不需要重复下载已经存在的依赖项,因此安装速度更快。
  • 避免幽灵依赖:由于 node_modules 文件夹中只有明确声明的依赖项的符号链接,因此避免了幽灵依赖的问题。
  • 严格性:由于所有依赖都在.pnpm文件夹下以规范化方式存储,不存在依赖提升,所以一个包无法访问到未声明的依赖。

2.2 硬链接 vs 符号链接

  • 硬链接
    • 优点:更节省空间,因为它们不占用额外的 inode。即使原始文件被删除,硬链接仍然有效。
    • 缺点:不能跨文件系统使用。
  • 符号链接
    • 优点:可以跨文件系统使用。
    • 缺点:占用额外的 inode。如果原始文件被删除,符号链接会失效。

pnpm 默认使用硬链接,但在某些情况下(例如,跨文件系统或不支持硬链接的环境)会回退到符号链接。

3. pnpm 的优势

总结一下,pnpm 相比于 npm 和 Yarn,具有以下显著优势:

  • 极大地节省磁盘空间:通过内容寻址存储和硬链接/符号链接,避免了依赖项的重复安装。
  • 显著加速安装:无需重复下载已存在的依赖项,大大减少了安装时间。
  • 避免幽灵依赖node_modules 结构更清晰,只包含明确声明的依赖项。
  • 更严格的依赖管理:通过 .pnpm 文件夹和规范化的存储方式,确保依赖关系的明确性和一致性。
  • 内置对 monorepo 的支持:pnpm 提供了一套完整的命令和工作区(workspaces)功能,可以轻松管理多包项目。
  • 兼容性:pnpm 的命令和用法与 npm 和 Yarn 非常相似,迁移成本低。

4. pnpm 的使用

pnpm 的安装和使用非常简单。

4.1. 安装 pnpm

您可以通过多种方式安装 pnpm:

  • 使用 npm 安装
    bash
    npm install -g pnpm

  • 使用 Corepack(Node.js 16.13+):
    bash
    corepack enable

    Corepack 是 Node.js 内置的包管理器管理器,它可以自动为您安装和管理 pnpm、Yarn 和 npm。

  • 独立脚本(适用于不安装 Node.js 的情况):

    • 在 Linux 或 macOS 上:
      bash
      curl -fsSL https://get.pnpm.io/install.sh | sh -
    • 在 Windows 上(PowerShell):
      powershell
      iwr https://get.pnpm.io/install.ps1 -useb | iex

4.2. 常用命令

pnpm 的命令与 npm 和 Yarn 非常相似,以下是一些常用命令的对比:

操作 npm Yarn pnpm
安装所有依赖 npm install yarn install pnpm install
安装单个依赖 npm install <package> yarn add <package> pnpm add <package>
安装开发依赖 npm install -D <package> yarn add -D <package> pnpm add -D <package>
全局安装 npm install -g <package> yarn global add <package> pnpm add -g <package>
删除依赖 npm uninstall <package> yarn remove <package> pnpm remove <package>
更新依赖 npm update yarn upgrade pnpm update
运行脚本 npm run <script> yarn <script> pnpm <script>
列出已安装的依赖 npm ls yarn list pnpm ls
发布包 npm publish yarn publish pnpm publish

4.3. pnpm 的配置文件

pnpm 的配置文件是 .npmrc,与 npm 相同。您可以在 .npmrc 文件中配置 pnpm 的行为,例如:

  • store-dir:指定全局存储库的路径。
  • link-workspace-packages:控制是否在工作区中链接本地包。
  • shamefully-hoist: 默认值为false。设置为true时,会创建扁平的 node_modules 结构,类似 npm 或 yarn classic.

4.4 工作区 (workspace)

pnpm 内置了对 monorepo 的支持, 通过工作区功能, 你可以在一个代码仓库中管理多个项目。

要设置工作区,您需要在仓库的根目录中创建 pnpm-workspace.yaml 文件,指定包含项目的目录:

yaml
packages:
- 'packages/*' # 所有在 packages 目录下的子目录
- 'components/*' # 也可以指定多个目录
- '!**/test/**' # 排除某些目录

在工作区中, 您可以使用 pnpm -r--recursive 选项在所有项目中执行命令,例如:

bash
pnpm install -r # 在所有项目中安装依赖
pnpm run build -r # 在所有项目中运行 build 脚本

您还可以使用过滤器来选择特定的项目执行命令,例如:

bash
pnpm --filter my-package run build # 只在 my-package 项目中运行 build 脚本
pnpm --filter ./packages/* run test # 在 packages 目录下的所有项目中运行 test 脚本

pnpm也支持--filter ...这种形式的过滤器, 比如:
pnpm --filter ...@foo/bar run build # 会选择@foo/bar以及@foo/bar依赖的所有包, 和依赖@foo/bar的所有包

5. 与 npm 和 Yarn 的对比

特性 pnpm npm Yarn (Classic) Yarn (Berry)
依赖管理方式 内容寻址存储 + 硬链接/符号链接 扁平化 + 嵌套(部分) 扁平化 + 嵌套(部分) Plug'n'Play (PnP)
磁盘空间占用 非常节省 较大 较大 较小(但仍有 node_modules
安装速度 非常快 较慢 较快
幽灵依赖
依赖关系严格性 非常严格 较宽松 较宽松 严格
Monorepo 支持 内置 需要 Lerna 等第三方工具 内置工作区 内置工作区
配置文件 .npmrc .npmrc .yarnrc .yarnrc.yml
兼容性 与 npm 和 Yarn 命令高度兼容 - 与 npm 命令高度兼容 与 npm 命令部分兼容,需要 yarn node 运行脚本

从上表可以看出,pnpm 在磁盘空间占用、安装速度、依赖关系严格性和 Monorepo 支持方面具有明显优势。

6. 总结

pnpm 是一款现代化的包管理器,它通过内容寻址存储和硬链接/符号链接的创新方式,解决了传统包管理器在依赖管理方面的诸多问题。它不仅可以显著节省磁盘空间,还能大幅提升安装速度,并避免幽灵依赖的风险。pnpm 的命令和用法与 npm 和 Yarn 高度兼容,迁移成本低,同时内置了对 Monorepo 的支持。如果您正在寻找一款更快速、更高效、更可靠的包管理器,pnpm 绝对值得一试。

希望这篇文章能帮助您全面了解 pnpm,并将其应用到您的项目中。

THE END