原文链接:Why should we use pnpm?,2017.03.19,by Zoltan Kochan
pnpm 是 Node.js 的一种替代包管理器,它可以完全取代 npm,而且更快、更高效。
有多快呢?比 npm 快 3 倍!可以在这里查看基准测试结果(benchmarks)。
为什么更高效呢?在你安装一个包时,我们会将其保存在你机器上的全局存储(global store)中,并通过创建硬链接(hard link)来引用它,而不是通过文件复制方式。对于每个模块的版本,磁盘上只保留唯一一个副本。然而,使用 npm 或 yarn 时,如果有 100 个包都依赖 lodash 库,那么磁盘上就会 100 份 lodash 的拷贝。pnpm 能够帮助你节省数 GB 的磁盘空间!
为什么不用 Yarn 呢?
说实话,当 Yarn 公开发布时,我真的感到非常失望。我在这几个月里一直积极地为 pnpm 做贡献,但关于 Yarn 的任何消息都没有出现过,它的开发信息并不对外公开。
几天后,我意识到 Yarn 只是相对于 npm 的一个小改进。虽然它可以加快安装速度,并且有一些很好的新功能,但它使用了与 npm 相同的扁平化 node_modules 结构(自版本 3 开始)。
而扁平化依赖树(flattened dependency trees)结构也带来了一系列问题:
- 可以在模块中访问到没有依赖的包(即没有写在
package.json文件中的依赖) - 扁平化依赖树的算法非常复杂
- 某些包必须被复制到一个项目的
node_modules文件夹中
此外,Yarn 并不打算解决一些问题,比如磁盘空间的使用问题。因此我决定继续投入时间来支持 pnpm,并且取得了巨大的成功。截至目前(2017 年 3 月),pnpm 已经具备了 Yarn 相对于 npm 的所有附加功能:
- 安全性。与 Yarn 类似,pnpm 有一个特殊文件,其中包含所有已安装软件包的校验码(checksums),方便在执行代码前验证每个已安装软件包的完整性
- 离线模式。pnpm 将所有下载的软件包 tarball 保存在本地注册表镜像中(local registry mirror)。当本地存在某个软件包时,它永远不会发出请求。通过使用
--offline参数,可以完全禁止 HTTP 请求 - 速度快。pnpm 不仅比 npm 更快,而且比 Yarn 更快,无论是冷启动(cold)还是热启动(hot cache)。Yarn 会从缓存中复制文件,而 pnpm 只需从全局存储库中链接它们。
怎么做到的?
正如我之前提到的,pnpm 不会将依赖树展平。因此,pnpm 使用的算法要简单得多!这就是为什么只有 1 个开发者能够跟上 Yarn 数十个贡献者的步伐。
那么,如果不是通过展平来组织 node_modules 目录,pnpm 又是如何做到的呢?要理解这一点,我们回忆一下 npm 版本 3 之前 node_modules 的文件夹结构:在 npm@3 之前,node_modules 结构是可预测且清晰的,因为每个依赖都有自己的 node_modules 文件夹,并在 package.json 中指定了所有它所依赖的其他模块。
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
这种方法存在两个严重的问题:
- 频繁地创建过深的依赖树,导致在 Windows 上出现了长目录路径问题(一般工具软件会有 256 字符长度限制)
- 当不同的依赖项依赖了同一个包时,这个包会被多次复制粘贴
为了解决这些问题,npm 重新思考了 node_modules 结构,并提出了扁平化处理方案。从 npm@3 开始,node_modules 结构如下所示:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
有关 npm v3 依赖解析的更多信息,请参阅这里的文章。
与 npm@3 不同,pnpm 试图解决 npm@2 存在的问题,但不需要扁平化依赖树。由 pnpm 创建的node_modules 文件夹中,所有包都将自己的依赖项分组在一起,但目录树永远不会像 npm@2 那样深。pnpm保持所有依赖项扁平化,但使用符号链接(symlinks)将它们分组在一起。
-> - 表示一个符号链接 (在 Windows 上叫 junction)
node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ foo/1.0.0/node_modules
| ├─ bar -> ../../bar/2.0.0/node_modules/bar
| └─ foo
| ├─ index.js
| └─ package.json
└─ bar/2.0.0/node_modules
└─ bar
├─ index.js
└─ package.json
要查看一个实际案例,可以访问 pnpm 样本项目仓库。
译注:现在 pnpm 已经不再使用这种目录结构了,而是代之以
.pnpm/foo@1.0.0/node_modules/foo方式,当然,也只是微调。因为.pnpm具有标识意义,类似于项目中的.vscode、.github的作用。
虽然这个案例对于一个小项目来说似乎过于复杂,但对于更大的项目来说,它的结构比 npm/yarn 创建的结构更好。我们来看看它的工作原理。
首先,你可能注意到了,node_modules 根路径中的包只是一个符号链接。这没问题,因为 Node.js 会忽略符号链接并执行真实路径。所以,require('foo') 会执行node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/foo/index.js 中的文件,而不是 node_modules/foo/index.js。
其次,安装下来的包的目录中并没有自己的 node_modules 文件夹,那么 foo 如何引用 bar 呢?让我们来看一下 foo 包中包含的文件夹:
node_modules/.registry.npmjs.org/foo/1.0.0/node_modules
├─ bar -> ../../bar/2.0.0/node_modules/bar
└─ foo
├─ index.js
└─ package.json
我们看到:
foo的依赖项(其实就是bar)已经安装,但在目录结构中向上提升了一级- 这两个包都在一个名为
node_modules的文件夹中
foo 可以引用到 bar,因为 Node.js 会在目录结构中查找模块,一直到磁盘根目录。而且 foo 也可以引用 foo,因为它位于一个名为 node_modules 的文件夹中(是的,有些包就是这么做的)。
你相信吗?
只需通过 npm 安装 pnpm:npm install -g pnpm 。每当想要安装某个东西时,就使用 pnpm 而不是npm:pnpm i foo。
此外,你可以在 pnpm 的 GitHub 仓库或 pnpm.js.org 上阅读更多信息。还可以在 Twitter 上关注 pnpm,或者在 pnpm Gitter 聊天室寻求帮助。