认识
一、认识
PNPM
是一个非常符合现代前端需求的工具。PNPM(Performant Node Package Manager)
是一个用于管理 Node.js
项目依赖的包管理工具。它被设计用于替代传统的 NPM(Node Package Manager)
,以提供更高效的依赖管理和更好的性能。PNPM
的核心理念是通过 符号链接 和 全局存储 来优化 依赖管理,解决 NPM
在大型项目中常见的性能和管理问题。
PNPM Monorepo
pnpm install
之前: 如所示, 根目录下的 node_modules
目录中只有直接依赖项,使用符号链接来创建依赖项的嵌套结构。所以 PNPM
的 node_modules
结构为非扁平结构。
pnpm-monorepo/
├── apps/
│ └── appA/
│ └── package.json // lodash@4.17.21 axios@1.7.8 react
│ └── appB/
│ └── package.json // lodash@4.17.15 axios@1.7.0 react
├── pnpm-workspace.yaml // 将 appA 和 appB 加入工作空间
└── package.json // lodash@4.17.9 axios@1.6.5 react
PNPM Monorepo
pnpm install
完成:
pnpm-monorepo/
├── node_modules/
│ └── .pnpm/ // node_modules/.pnpm 包含实际内容的物理文件
│ └── asynckit@0.4.0
│ └── node_modules
│ └── asynckit
│ └── axios@1.6.5
│ └── node_modules
│ └── axios
│ └── follow-redirects
│ └── follow-redirects
│ └── proxy-from-env
│ └── axios@1.7.0
│ └── node_modules
│ └── axios
│ └── follow-redirects
│ └── follow-redirects
│ └── proxy-from-env
│ └── axios@1.7.8
│ └── node_modules
│ └── axios
│ └── follow-redirects
│ └── follow-redirects
│ └── proxy-from-env
│ └── combined-stream@1.0.8
│ └── node_modules
│ └── combined-stream
│ └── delayed-stream@1.0.0
│ └── node_modules
│ └── delayed-stream
│ └── follow-redirects@1.15.9
│ └── node_modules
│ └── follow-redirects
│ └── form-data@4.0.1
│ └── node_modules
│ └── form-data
│ └── lodash@4.17.9
│ └── node_modules
│ └── lodash
│ └── lodash@4.17.15
│ └── node_modules
│ └── lodash
│ └── lodash@4.17.21
│ └── node_modules
│ └── lodash
│ └── mime-db@1.52.0
│ └── node_modules
│ └── mime-db
│ └── mime-types@2.1.35
│ └── node_modules
│ └── mime-types
│ └── proxy-from-env@1.1.0
│ └── node_modules
│ └── proxy-from-env
│ └── react@19.0.0
│ └── node_modules
│ └── react
│ └── delayed-stream@1.0.0
│ └── node_modules
│ └── delayed-stream
│ └── axios // 符号链接 指向 .pnpm axios@1.6.5
│ └── lodash // 符号链接 指向 .pnpm lodash@4.17.9
│ └── react // 符号链接 指向 .pnpm react@19.0.0
├── apps/
│ └── appA/
│ └── node_modules/
│ └── axios // 符号链接 指向 .pnpm axios@1.7.8
│ └── lodash // 符号链接 指向 .pnpm lodash@4.17.21
│ └── react // 符号链接 指向 .pnpm react@19.0.0
│ └── package.json // lodash@4.17.21 axios@1.7.8 react
│ └── appB/
│ └── node_modules/
│ └── axios // 符号链接 指向 .pnpm axios@1.7.0
│ └── lodash // 符号链接 指向 .pnpm lodash@4.17.15
│ └── react // 符号链接 指向 .pnpm react@19.0.0
│ └── package.json // lodash@4.17.15 axios@1.7.0 react
├── pnpm-workspace.yaml // 将 appA 和 appB 加入工作空间
└── package.json // lodash@4.17.9 axios@1.6.5 react
因此 pnpm install
过程如下:
-
解析工作空间:
PNPM
读取pnpm-workspace.yaml
文件,将apps/appA
和apps/appB
加入工作空间(Workspace
),确保依赖共享。 -
解析依赖树: 分别解析根
package.json
和每个子项目的package.json
,接着,它会解析出这些依赖项的依赖树,构建整个项目的全局依赖树, 确保所有的依赖项及其子依赖项都被正确解析和记录。 -
检查和下载包:
PNPM
会检查全局存储中(通常在~/.pnpm-store
目录下)是否已经存在所需版本的包。如果所需包已经存在,它将跳过下载步骤。如果包不存在,PNPM
会从注册表(通常是npm registry
)中下载包并存储到全局存储中。PNPM
的依赖存储利用了 内容寻址存储(Content Addressable Storage
), 每个包会基于其内容的哈希生成唯一的标识符,用于存储在全局缓存中(默认位于~/.pnpm-store
)。如果某个版本的依赖已经存在于全局缓存,PNPM
会直接复用,而不重新下载。下载或缓存的依赖会解压到node_modules/.pnpm
中。 -
创建
node_modules/.pnpm
: 在项目的node_modules
目录下,PNPM
会创建一个.pnpm
子目录。这个目录将包含所有依赖项的实际包内容,按照包名和版本号进行组织。每个包的目录中还会包含其自身的node_modules
目录,其中存储着该包的依赖项。 -
创建符号链接:
PNPM
会在项目的node_modules
目录中创建符号链接,这些链接指向.pnpm
目录中的实际包内容。这样,当你在项目中导入一个依赖项时,Node.js
会通过符号链接找到实际的包内容。PNPM
的依赖存储利用了 内容寻址存储(Content Addressable Storage
), 每个包会基于其内容的哈希生成唯一的标识符,用于存储在全局缓存中(默认位于~/.pnpm-store
)。PNPM
随后通过 分离式依赖树 (Flat Dependency Tree
) 通过符号链接实现不同版本的依赖共存, 实现了 依赖隔离, 比如lodash@4.17.9
、lodash@4.17.15
和lodash@4.17.21
都被存储,因为它们的版本不同。相同的依赖内容只存储一次,实现了 依赖共享, 例如,follow-redirects
是axios
的依赖,它虽然出现在多个axios
版本中,但在.pnpm
里只存储一次(如follow-redirects@1.15.9
)。 -
生成
pnpm-lock.yaml
文件:PNPM
会生成或更新项目的pnpm-lock.yaml
文件(类似于NPM
的package-lock.json
文件)。这个文件记录了所有安装的依赖项及其版本,以及它们的依赖关系。这有助于确保在未来安装或更新依赖项时能够重现相同的依赖树。 -
最终的非扁平
node_modules
目录结构:PNPM
创建 符号链接 来构建嵌套的依赖关系图过程: 1. 直接依赖, 依赖首先 硬链接 到node_modules/.pnpm
, 随后 符号链接 到node_modules
下,node_modules/.pnpm
是依赖的实际存放位置, 所以,node_modules
下只会存在 直接依赖 (package.json
中指定的依赖), 所以, 项目中不能直接访问 非直接依赖, 有效的解决了 幽灵依赖问题。2. 子级依赖(次级依赖): 将 直接依赖 依赖的 子级依赖(次级依赖) 硬链接 到node_modules/.pnpm
, 然后 软链接 到与 直接依赖 同一级别node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
。所以,PNPM
使用软链与平铺目录来构建一个嵌套结构。而且, 直接依赖 与 直接依赖的次级依赖(子依赖) 一起放到一个 ``node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
PNPM
的优势:
1. 节省磁盘空间: 使用 NPM
或者 Yarn
时,依赖每次被不同的项目使用,都会重复安装一次。 而 PNPM
的依赖存储利用了 内容寻址存储(Content Addressable Storage
), 每个包会基于其内容的哈希生成唯一的标识符,用于存储在全局缓存中(默认位于 ~/.pnpm-store
)。如果某个版本的依赖已经存在于全局缓存,PNPM
会直接复用,而不重新下载。这个依赖会 硬链接 到 node_modules/.pnpm
, 这些是唯一真实的文件, 就会创建 符号链接 来构建嵌套的依赖关系图结构。因此,你在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
2. 提高安装速度: pnpm install
在解析、构建完依赖树 - 目录结构计算之后, 链接依赖项, 这时候, 所有以前安装过的依赖项都会直接从存储区中获取并链接到 node_modules
。这种方法比传统的三阶段安装过程(解析、获取和写入所有依赖项到 node_modules
)要快得多。
3. 非扁平的 node_modules
目录: 使用 npm
或 Yarn Classic
安装依赖项时,所有的包都被提升到模块目录的根目录。 这样就导致了一个问题,源码可以直接访问和修改依赖,而不是作为只读的项目依赖, 造成了幽灵依赖的问题。默认情况下, PNPM
node_modules
布局使用 符号链接 来创建依赖项的 嵌套结构(非扁平结构), 一旦依赖 硬链接 到 node_modules/.pnpm
,就会创建 符号链接 来构建嵌套的依赖关系图结构。创建 符号链接 来构建嵌套的依赖关系图过程: 1. 直接依赖, 依赖首先 硬链接 到 node_modules/.pnpm
, 随后 符号链接 到 node_modules
下, node_modules/.pnpm
是依赖的实际存放位置, 所以, node_modules
下只会存在 直接依赖 (package.json
中指定的依赖), 所以, 项目中不能直接访问 非直接依赖, 有效的解决了 幽灵依赖问题。2. 子级依赖(次级依赖): 将 直接依赖 依赖的 子级依赖(次级依赖) 硬链接 到 node_modules/.pnpm
, 然后 软链接 到与 直接依赖 同一级别 node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
。所以, PNPM
使用软链与平铺目录来构建一个嵌套结构。而且, 直接依赖 与 直接依赖的次级依赖(子依赖) 一起放到一个 ``node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
node_modules` 文件夹, 它们处于同级目录, 可以避免循环符号链接。
4. 严格的版本控制和依赖隔离: PNPM
的依赖存储利用了 内容寻址存储(Content Addressable Storage
), 每个包会基于其内容的哈希生成唯一的标识符,用于存储在全局缓存中(默认位于 ~/.pnpm-store
)。PNPM
随后通过 分离式依赖树 (Flat Dependency Tree
) 通过符号链接实现不同版本的依赖共存, 实现了 依赖隔离, 比如 lodash@4.17.9
、lodash@4.17.15
和 lodash@4.17.21
都被存储,因为它们的版本不同。相同的依赖内容只存储一次,实现了 依赖共享, 例如,follow-redirects
是 axios
的依赖,它虽然出现在多个 axios
版本中,但在 .pnpm
里只存储一次(如 follow-redirects@1.15.9
)。
二、核心
2.1 硬链接
硬链接(Hard Link
) 是在文件系统中创建的指向某个文件数据块的引用。与符号链接不同,硬链接是文件系统级别的链接,它们指向相同的物理数据块,而不是文件的路径。因此,硬链接是对文件内容的直接引用,而符号链接是对文件路径的引用。
PNPM
使用硬链接来提高性能和节省磁盘空间。具体来说,PNPM
在安装依赖时,会在项目的 node_modules
目录中创建指向全局存储中实际包文件的硬链接。这样做的好处是避免了重复下载和存储相同的包内容,同时提高了文件访问的效率。
PNPM
硬链接的工作原理
-
全局存储: 当第一次下载某个依赖包时,
PNPM
会将其存储在全局存储目录中(通常在~/.pnpm-store
)。 -
创建硬链接: 在项目的
node_modules
目录中,PNPM
会创建指向全局存储中相应包文件的硬链接。这些硬链接指向相同的数据块,因此不会占用额外的磁盘空间。 -
依赖结构: 使用硬链接后,项目的
node_modules
目录中依赖项看起来像是独立文件,但实际上它们共享相同的数据块。
PNPM
硬链接优点
-
节省磁盘空间: 相同版本的包只会在全局存储中保存一次,通过硬链接共享这些包,避免重复存储,节省了磁盘空间。
-
提高文件访问速度: 由于硬链接是文件系统级别的引用,文件访问速度比符号链接更快。
-
一致的数据管理: 修改硬链接或原文件,数据同步更新,确保了一致性。
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
的工作流程如下:
-
解析依赖树: 读取
package.json
文件并解析所有的依赖项及其版本要求。 -
检查和下载包: 检查全局存储中是否已经存在所需版本的包,如果不存在则下载这些包并存储到全局存储中。
-
创建
.pnpm
目录: 在项目的node_modules
目录下创建.pnpm
子目录,并将所有包存储在这个子目录中,按照包名和版本号进行组织。 -
创建符号链接: 在
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
目录中,express
和lodash
是符号链接,指向.pnpm
目录中的实际包内容。
PNPM
符号链接优点
-
节省磁盘空间: 相同版本的包只会在
.pnpm
目录中存储一次,通过符号链接共享这些包,避免重复安装,节省了磁盘空间。 -
依赖隔离: 符号链接确保了每个包使用其特定版本的依赖项,避免了版本冲突和相互干扰
-
提高安装速度: 全局存储和符号链接机制避免了重复下载和解压包,提高了安装速度
-
一致的依赖管理: 符号链接和
.pnpm
目录结构使得依赖关系更加清晰和可控,便于管理和调试。
2.3 隔离依赖
隔离依赖: 每个包的依赖项存储在 .pnpm
子目录中,并且包之间的依赖关系通过符号链接来管理。这种方式确保了依赖项之间的严格隔离,避免了不同包之间的版本冲突。
2.4 全局存储
全局存储: PNPM
使用一个全局存储(例如 ~/.pnpm-store
)来存放所有安装过的包。
三、问题
3.1 PNPM 的弊端?
-
调试问题: 所有项目引用的包都在全局一个地方,如果想对某个包进行调试,其他项目正好引用了,本地运行也会收到影响。
-
兼容问题: 即软连接的方式可能会在
windows
存在一些兼容的问题,但是针对这个问题,pnpm
也提供了对应的解决方案:在win
系统上使用一个叫做junctions
的特性来替代软连接,这个方案在window
上的兼容性要好于symlink
3.2 npm vs cnpm vs yarn vs pnpm
npm
: NPM
默认将所有依赖提升到根级 node_modules
, 如果子项目的依赖版本与根项目的依赖冲突, NPM
会在子项目的 node_modules
中直接安装该版本,保证依赖的隔离性。所有依赖不再具有嵌套的 node_modules
文件夹,完全扁平化。根级的 node_modules
是全局共享的, 相同版本的依赖在整个 Monorepo
中只安装一次,极大地减少了磁盘空间浪费。子项目的 node_modules
文件夹只包含对其独有依赖的软链接(与根项目的依赖冲突的依赖)。扁平化的结构避免了传统 NPM
中依赖深层嵌套的问题,依赖查找更加高效,避免了传统 NPM
中长路径导致的问题。所有的包都被提升到模块目录的根目录。 这样就导致了一个问题,源码可以直接访问和修改依赖,而不是作为只读的项目依赖, 造成了幽灵依赖的问题。
cnpm
: cnpm
不生成 lock
文件,也不会识别项目中的lock
文件
yarn
: Yarn
默认将所有依赖提升到根级 node_modules
, 如果子项目的依赖版本与根项目的依赖冲突, Yarn
会在子项目的 node_modules
中直接安装该版本,保证依赖的隔离性。所有依赖不再具有嵌套的 node_modules
文件夹,完全扁平化。根级的 node_modules
是全局共享的, 相同版本的依赖在整个 Monorepo
中只安装一次,极大地减少了磁盘空间浪费。子项目的 node_modules
文件夹只包含对其独有依赖的软链接(与根项目的依赖冲突的依赖)。扁平化的结构避免了传统 NPM
中依赖深层嵌套的问题,依赖查找更加高效,避免了传统 NPM
中长路径导致的问题。所有的包都被提升到模块目录的根目录。 这样就导致了一个问题,源码可以直接访问和修改依赖,而不是作为只读的项目依赖, 造成了幽灵依赖的问题。
pnpm
: PNPM
使用了一种非扁平结构,PNPM
创建 符号链接 来构建嵌套的依赖关系图过程: 1. 直接依赖, 依赖首先 硬链接 到 node_modules/.pnpm
, 随后 符号链接 到 node_modules
下, node_modules/.pnpm
是依赖的实际存放位置, 所以, node_modules
下只会存在 直接依赖 (package.json
中指定的依赖), 所以, 项目中不能直接访问 非直接依赖, 有效的解决了 幽灵依赖问题。2. 子级依赖(次级依赖): 将 直接依赖 依赖的 子级依赖(次级依赖) 硬链接 到 node_modules/.pnpm
, 然后 软链接 到与 直接依赖 同一级别 node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
。所以, PNPM
使用软链与平铺目录来构建一个嵌套结构。而且, 直接依赖 与 直接依赖的次级依赖(子依赖) 一起放到一个 ``node_modules/.pnpm/直接依赖/node_modules/ 直接依赖,子级依赖……
node_modules 文件夹, 它们处于同级目录, 可以避免循环符号链接。**
PNPM** 通过 **分离式依赖树 (
Flat Dependency Tree)** 通过符号链接实现不同版本的依赖共存, 实现了 **依赖隔离**, 比如
lodash@4.17.9、
lodash@4.17.15和
lodash@4.17.21 都被存储,因为它们的版本不同。相同的依赖内容只存储一次,实现了 **依赖共享**, 例如,
follow-redirects是
axios的依赖,它虽然出现在多个
axios版本中,但在
.pnpm里只存储一次(如
follow-redirects@1.15.9)。 **
PNPM** 会生成
pnpm-lock.json` 文件。
3.3 NPM node_modules 扁平结构和 PNPM node_modules 的非扁平结构式如何理解的?
NPM
扁平结构: 在 NPM
的扁平结构中,node_modules
目录尽可能地将依赖项安装在根目录中,以减少嵌套层级。这种扁平化的安装方式旨在避免过深的目录结构和长路径问题。NPM
扁平结构 可以避免过深的目录嵌套,减少了路径长度的问题。安装方式也很简单。但是,可能导致版本冲突。多个包依赖不同版本的同一依赖项时,NPM
可能会将它们扁平化到同一目录下,造成版本冲突。而且重复安装依赖项。相同的依赖项可能在多个项目中重复下载和安装,浪费磁盘空间。
PNPM
非扁平结构: PNPM
使用了一种非扁平结构,它在 node_modules
目录下创建一个 .pnpm
子目录,实际的包内容存储在这个子目录中,然后通过 符号链接(symlink
) 将依赖项链接到项目的 node_modules
目录中。这种结构确保了依赖项的隔离,避免了版本冲突。PNPM
非扁平结构 通过 符号链接 和 .pnpm
目录隔离依赖项,确保不同项目之间的依赖项互不干扰,避免版本冲突, 全局存储和符号链接机制避免了重复安装相同版本的包, 节省了磁盘空间。全局存储和并行安装提高了安装速度。但是,PNPM
需要处理符号链接,可能会对一些工具和脚本造成兼容性问题。