跳到主要内容

认识

2023年05月26日
柏拉文
越努力,越幸运

一、认识


PNPM(Performant Node Package Manager) 是一个用于管理 Node.js 项目依赖的包管理工具。它被设计用于替代传统的 NPM(Node Package Manager),以提供更高效的依赖管理和更好的性能。PNPM 的核心理念是通过 符号链接全局存储 来优化 依赖管理,解决 NPM 在大型项目中常见的性能和管理问题。

二、核心


2.1 硬链接

硬链接(Hard Link 是在文件系统中创建的指向某个文件数据块的引用。与符号链接不同,硬链接是文件系统级别的链接,它们指向相同的物理数据块,而不是文件的路径。因此,硬链接是对文件内容的直接引用,而符号链接是对文件路径的引用。

PNPM 使用硬链接来提高性能和节省磁盘空间。具体来说,PNPM 在安装依赖时,会在项目的 node_modules 目录中创建指向全局存储中实际包文件的硬链接。这样做的好处是避免了重复下载和存储相同的包内容,同时提高了文件访问的效率。

PNPM 硬链接的工作原理

  1. 全局存储: 当第一次下载某个依赖包时,PNPM 会将其存储在全局存储目录中(通常在 ~/.pnpm-store)。

  2. 创建硬链接: 在项目的 node_modules 目录中,PNPM 会创建指向全局存储中相应包文件的硬链接。这些硬链接指向相同的数据块,因此不会占用额外的磁盘空间。

  3. 依赖结构: 使用硬链接后,项目的 node_modules 目录中依赖项看起来像是独立文件,但实际上它们共享相同的数据块。

PNPM 硬链接优点

  1. 节省磁盘空间: 相同版本的包只会在全局存储中保存一次,通过硬链接共享这些包,避免重复存储,节省了磁盘空间。

  2. 提高文件访问速度: 由于硬链接是文件系统级别的引用,文件访问速度比符号链接更快。

  3. 一致的数据管理: 修改硬链接或原文件,数据同步更新,确保了一致性。

2.2 符号链接

PNPM 中,符号链接(Symbolic Links, symlinks 是用于在文件系统中创建指向另一个文件或目录的引用。这种机制允许在一个位置创建一个链接,指向存储在另一个位置的实际内容。在 PNPM 的上下文中,符号链接用于将 node_modules 目录中的依赖项链接到存储在 .pnpm 子目录中的实际包内容。这种方法有助于高效地管理依赖项,节省磁盘空间,并避免版本冲突。

符号链接: 每个项目的 node_modules 目录中的依赖项不是实际的包副本,而是指向全局存储中相应包的符号链接。PNPM 使用符号链接将项目的 node_modules 目录中的包链接到 .pnpm 目录中存储的实际包内容。这意味着在项目的 node_modules 目录中看到的包实际上是指向 .pnpm 目录中存储的内容的快捷方式。

PNPM 符号链接的工作原理

假设我们有一个项目,其 package.json 文件如下:

{
"dependencies": {
"express": "^4.17.1",
"lodash": "^4.17.21"
}
}

当运行 pnpm install 时,PNPM 的工作流程如下:

  1. 解析依赖树: 读取 package.json 文件并解析所有的依赖项及其版本要求。

  2. 检查和下载包: 检查全局存储中是否已经存在所需版本的包,如果不存在则下载这些包并存储到全局存储中。

  3. 创建 .pnpm 目录: 在项目的 node_modules 目录下创建 .pnpm 子目录,并将所有包存储在这个子目录中,按照包名和版本号进行组织。

  4. 创建符号链接: 在 node_modules 目录中创建指向 .pnpm 目录中相应包的符号链接。

生成的目录结构:

project
└── node_modules
├── .pnpm
│ ├── express@4.17.1
│ │ ├── node_modules
│ │ │ └── ...
│ ├── lodash@4.17.21
│ └── ...(其他依赖)
├── express -> .pnpm/express@4.17.1/node_modules/express
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
  • .pnpm 目录:实际的包内容存储在 node_modules/.pnpm 子目录中,按照包名和版本号进行组织。

  • 符号链接: 在 node_modules 目录中,expresslodash 是符号链接,指向 .pnpm 目录中的实际包内容。

PNPM 符号链接优点

  1. 节省磁盘空间: 相同版本的包只会在 .pnpm 目录中存储一次,通过符号链接共享这些包,避免重复安装,节省了磁盘空间。

  2. 依赖隔离: 符号链接确保了每个包使用其特定版本的依赖项,避免了版本冲突和相互干扰

  3. 提高安装速度: 全局存储和符号链接机制避免了重复下载和解压包,提高了安装速度

  4. 一致的依赖管理: 符号链接和 .pnpm 目录结构使得依赖关系更加清晰和可控,便于管理和调试。

2.3 隔离依赖

隔离依赖: 每个包的依赖项存储在 .pnpm 子目录中,并且包之间的依赖关系通过符号链接来管理。这种方式确保了依赖项之间的严格隔离,避免了不同包之间的版本冲突。

2.4 全局存储

全局存储: PNPM 使用一个全局存储(例如 ~/.pnpm-store)来存放所有安装过的包。

三、特点


3.1 节省磁盘空间和带宽

NPM 在每个项目中都会安装和保存相同依赖的多个副本,导致大量磁盘空间被占用。而 PNPM 通过全局存储和符号链接机制避免了重复安装相同版本的包,大大节省了磁盘空间。

  • 符号链接机制: PNPM 使用符号链接来管理依赖,这意味着相同版本的依赖项只会在全局存储中保存一次,而不会在每个项目中重复存储,大大节省了磁盘空间。

  • 快速的网络传输: 当一个包已经存在于全局存储中时,PNPM 只需创建符号链接,而不需要下载和安装该包的副本,节省了带宽和下载时间。

3.2 快速的安装和更新

NPM 每次安装相同的依赖项时都需要重新下载和解压。PNPM 由于使用了 符号链接全局存储并行安装 提高了安装速度。PNPM 在安装相同版本的依赖时无需重新下载和解压,大幅提升了安装速度。

3.3 严格的版本控制和依赖隔离

NPM 的扁平依赖结构可能导致版本冲突。PNPM 使用硬链接和符号链接来确保依赖项的版本隔离,即使不同项目依赖相同的包,它们也不会互相干扰,避免了版本冲突带来的问题。

3.4 创建非扁平的 node_modules 目录结构

创建非扁平的 node_modules 目录结构: PNPMnode_modules 目录结构与 NPM 不同,它不会创建一个完全扁平的目录结构,而是会在 node_modules 目录下创建一个 .pnpm 子目录来存储包的内容,并通过符号链接将依赖项链接到项目的 node_modules 目录中。这种结构确保了依赖项之间的严格隔离,避免版本冲突。

示例: 假设我们有一个项目,该项目依赖于 expresslodash 包,并且 express 依赖于 accepts 包。

  1. 安装依赖

    pnpm install express lodash
  2. 目录结构: 安装完成后,项目的目录结构可能如下:

    project
    └── node_modules
    ├── .pnpm
    │ ├── express@4.17.1
    │ │ ├── node_modules
    │ │ │ ├── accepts@1.3.7
    │ │ │ └── ...
    │ ├── lodash@4.17.21
    │ └── ...
    ├── express -> .pnpm/express@4.17.1/node_modules/express
    └── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
    • .pnpm 目录中包含 expresslodash 的实际包内容。

    • node_modules 目录中包含指向 .pnpm 子目录中相应包的符号链接。

    • express 的依赖(例如 accepts)也存储在 .pnpm 目录中,并且 express 包中包含指向其依赖的符号链接。

在使用 PNPM 时,项目的 node_modules 目录将通过符号链接指向 .pnpm 子目录中存储的实际包内容,确保依赖项之间的隔离和高效共享。这样可以减少磁盘空间的使用,并且在依赖项之间建立清晰的关系,避免版本冲突。

通过这种非扁平结构,PNPM 能够更高效地管理和共享依赖项,避免了重复安装相同版本的包,从而提升了安装速度和节省了磁盘空间。

四、工作


假设我们有一个项目,其中 package.json 文件中列出了以下依赖项:

{
"dependencies": {
"express": "^4.17.1",
"lodash": "^4.17.21"
}
}

运行 pnpm install 时,PNPM 的工作流程如下:

4.1 解析依赖树

PNPM 会首先读取项目的 package.json 文件,解析出所有的依赖项及其版本要求。接着,它会解析出这些依赖项的依赖树,确保所有的依赖项及其子依赖项都被正确解析和记录。

在本例中, PNPM 解析 expresslodash 的依赖关系。解析出 express 的子依赖项,例如 acceptsbody-parser 等。

4.2 检查和下载包

PNPM 会检查全局存储中(通常在 ~/.pnpm-store 目录下)是否已经存在所需版本的包。如果所需包已经存在,它将跳过下载步骤。如果包不存在,PNPM 会从注册表(通常是 npm registry)中下载包并存储到全局存储中。

在本例中, PNPM 检查全局存储中是否已经有 expresslodash 及其子依赖项。如果全局存储中没有这些包,PNPM 会从 npm registry 下载这些包,并存储到全局存储中。

4.3 创建 .pnpm 目录

在项目的 node_modules 目录下,PNPM 会创建一个 .pnpm 子目录。这个目录将包含所有依赖项的实际包内容,按照包名和版本号进行组织。每个包的目录中还会包含其自身的 node_modules 目录,其中存储着该包的依赖项。

在本例中,在项目的 node_modules 目录下创建 .pnpm 子目录。将 expresslodash 及其子依赖项存储到 .pnpm 目录中。

4.4 创建符号链接

PNPM 会在项目的 node_modules 目录中创建符号链接,这些链接指向 .pnpm 目录中的实际包内容。这样,当你在项目中导入一个依赖项时,Node.js 会通过符号链接找到实际的包内容。

在本例中,在项目的 node_modules 目录中创建指向 .pnpm 目录中相应包的符号链接。

project
└── node_modules
├── .pnpm
│ ├── express@4.17.1
│ │ ├── node_modules
│ │ │ ├── accepts@1.3.7
│ │ │ └── ...
│ ├── lodash@4.17.21
│ └── ...
├── express -> .pnpm/express@4.17.1/node_modules/express
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash

4.5 更新 shrinkwrap.yaml 文件

PNPM 会生成或更新项目的 shrinkwrap.yaml 文件(类似于 NPMpackage-lock.json 文件)。这个文件记录了所有安装的依赖项及其版本,以及它们的依赖关系。这有助于确保在未来安装或更新依赖项时能够重现相同的依赖树。

4.6 最终的 node_modules 非扁平目录结构

假设项目依赖于 expresslodash,并且 express 依赖于 accepts

project
└── node_modules
├── .pnpm
│ ├── accepts@1.3.7
│ ├── express@4.17.1
│ │ ├── node_modules
│ │ │ └── accepts@1.3.7
│ ├── lodash@4.17.21
│ └── ...(其他依赖)
├── accepts -> .pnpm/accepts@1.3.7/node_modules/accepts
├── express -> .pnpm/express@4.17.1/node_modules/express
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
  • .pnpm目录 所有包的实际内容存放在 .pnpm 目录中,按照包名和版本号进行组织。

  • node_modules 目录中包含指向 .pnpm 目录中实际包内容的符号链接。例如,expressnode_modules 中是一个指向 .pnpm/express@4.17.1/node_modules/express 的符号链接。

  • 同一版本的包只存储一次,通过符号链接进行共享,节省了磁盘空间。

PNPM 非扁平结构: PNPM 使用了一种非扁平结构,它在 node_modules 目录下创建一个 .pnpm 子目录,实际的包内容存储在这个子目录中,然后通过 符号链接(symlink 将依赖项链接到项目的 node_modules 目录中。这种结构确保了依赖项的隔离,避免了版本冲突。PNPM 非扁平结构 通过 符号链接.pnpm 目录隔离依赖项,确保不同项目之间的依赖项互不干扰,避免版本冲突, 全局存储和符号链接机制避免了重复安装相同版本的包, 节省了磁盘空间。全局存储和并行安装提高了安装速度。但是,PNPM 需要处理符号链接,可能会对一些工具和脚本造成兼容性问题。

六、问题


6.1 PNPM 的弊端?

  • 调试问题: 所有项目引用的包都在全局一个地方,如果想对某个包进行调试,其他项目正好引用了,本地运行也会收到影响。

  • 兼容问题: 即软连接的方式可能会在 windows 存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 window 上的兼容性要好于 symlink

6.2 NPM node_modules 扁平结构和 PNPM node_modules 的非扁平结构式如何理解的?

NPM 扁平结构: 在 NPM 的扁平结构中,node_modules 目录尽可能地将依赖项安装在根目录中,以减少嵌套层级。这种扁平化的安装方式旨在避免过深的目录结构和长路径问题。NPM 扁平结构 可以避免过深的目录嵌套,减少了路径长度的问题。安装方式也很简单。但是,可能导致版本冲突。多个包依赖不同版本的同一依赖项时,NPM 可能会将它们扁平化到同一目录下,造成版本冲突。而且重复安装依赖项。相同的依赖项可能在多个项目中重复下载和安装,浪费磁盘空间。

PNPM 非扁平结构: PNPM 使用了一种非扁平结构,它在 node_modules 目录下创建一个 .pnpm 子目录,实际的包内容存储在这个子目录中,然后通过 符号链接(symlink 将依赖项链接到项目的 node_modules 目录中。这种结构确保了依赖项的隔离,避免了版本冲突。PNPM 非扁平结构 通过 符号链接.pnpm 目录隔离依赖项,确保不同项目之间的依赖项互不干扰,避免版本冲突, 全局存储和符号链接机制避免了重复安装相同版本的包, 节省了磁盘空间。全局存储和并行安装提高了安装速度。但是,PNPM 需要处理符号链接,可能会对一些工具和脚本造成兼容性问题。

参考资料


从npm发展历程看pnpm的高效