为什么要使用 pnpm?

1,163 阅读6分钟

前言

这是一篇翻译自Zoltan Kochan编写的【Why should we use pnpm?】 的文章。以下提及的“我”均指pnpm的贡献者也就是作者Zoltan Kochan。此外,译文有些地方会额外多一些说明帮助更好的理解。

这将收录进我的专栏 《前端环境搭建与准备-指引》,用以让我的读者更好的了解pnpm配置。

相关网站/文章地址

译文正文

pnpmNode.js的另一个包管理器,它是npm的替代品,但它速度更快,效率更高。

pnpm有多快呢? 快3倍!请参阅 此处 的基准。

为什么pnpm的效率更高呢?当你安装一个依赖包时,pnpm会把这个依赖包放到你电脑上的全局缓存里,然后从中创建一个硬链接,而不是复制(npm或yarn是复制这个依赖包到 node_modules 里)。对于依赖包模块的每个版本,磁盘上只保留一个副本。例如,当我们使用npmyarn时,如果你的项目中使用了100个依赖包,这些依赖包都使用到了lodash,那么你的磁盘里就会有100份lodash的复制包。pnpm会让你节省千兆(字节)的磁盘空间!

为什么不使用Yarn?

说实话,Yarn发布的时候,“我”真的很失望。几个月来,“我”为pnpm做了大量的贡献,期间,没有任何关于yarn的新闻。关于其(yarn)的未来发展的信息并未公开。

几天之后,“我”才意识到,yarn仅仅是对npm进行了一个小改进而已。虽然它(yarn)让我们安装依赖包更快一点且有一些很不错的新功能,但是,yarn使用与npm(自版本3起)相同的扁平化 node_modules 结构。

扁平化依赖树带来了一系列问题,如下:

    1. 模块可以访问它们不依赖的包
    1. 依赖树的扁平化算法是相当复杂的
    1. 一些包必须被复制到一个项目的 node_modules 文件夹中

此外,还有一些问题Yarn并不打算解决,比如磁盘空间的使用问题。所以“我”决定继续把“我”的时间投入到pnpm,并取得了巨大的成功。截至目前(2017年3月),pnpm具有 yarn覆盖npm 的所有附加功能:

    1. 安全性 。和yarn 一样,pnpm 有一个特殊的文件,文件里包含所有已安装包的校验和,以此来确保在执行代码之前验证每个已安装包是否完整(包完整性)。
    1. 脱机模式 。pnpm将所有已下载好的包源码保存在本地注册表镜像中。当包在本地可用时,将不再发出请求去下载包。配置--offline参数,就可以完全禁止HTTP请求。
    1. 更快速 。 pnpm不仅比npm更快,还比yarn更快。pnpm比使用冷缓存和热缓存的yarn都快。Yarn是从缓存里复制文件到项目,而pnpm仅从全局存储中创建链接文件。

怎么可能呢?

就像“我”前面说的,pnpm不扁平化依赖树,因此,pnpm使用的算法简单得多!这就是为什么仅仅只需一个开发者就相当于Yarn的几十个贡献者。那么,如果不是通过扁平化的话,pnpm又是如何构造 node_modules 目录的呢?

为了理解它,我们要先回想一下在npm的版本3之前, node_modules 文件夹是什么样子。npm@3之前, node_modules 文件的结构是可预测且干净的,因为,每个依赖项的 node_modules 文件夹中都有它自己的 node_modules 文件夹,并在package.json中指明了所有依赖项(以此类推,node_modules 文件夹是可预测且干净整洁的)。例如:

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json

但是这样就会造成了两个严重的问题,如:

  • 软件包经常创建太深的依赖树,这导致Windows上出现长目录路径问题。

  • 当在不同的依赖项中使用同一个包时(例如多个依赖项都使用到了lodash ),那么同一个包会被复制粘贴了多次( lodash就会被复制粘贴多次到这些依赖项的node_modules文件夹里 )。

为了解决这个问题,npm团队重新考虑了 node_modules 结构并提出了扁平化。这就有了npm@3,其 node_modules 结构现如下所示:

node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json

有关npm v3依赖项解决方案的更多信息,请参阅npm v3 Dependency Resolution.

不同于npm@3的是,pnpm 尝试去解决 npm@2 已有的问题,而不是扁平化依赖树。通过pnpm创建的 node_modules 文件夹,所有包都有他们自己的依赖性组合在一起,但是目录树从来不会像 npm@2 一样那么深。pnpm平铺所有的依赖项,但是使用软链(symlinks)把这些依赖组合在一起。

a symlink (or junction on Windows)

软链(或者在windows上的链接),(我们也会称symlinks为“软链接”或“符号链接”,但是称为软链接似乎更为妥帖)

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

想要查看实时示例,请访问示例 sample pnpm project

虽然这个示例对于一个小项目来说似乎太复杂了,但是对于大型项目而言,这种结构比npm/yarn创建出来的结构看起来要更好些。让我们来看看原因吧。

首先 ,你应该可能已经注意到了, node_modules 根目录中的包只是一个软链接。这其实挺好的,因为Node.js忽略软连接并执行真实路径。所以require('foo') 将会执行 node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/foo/index.js这个路径的index.js文件,而不是 node_modules/foo/index.js这个路径的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)已安装,但在目录结构中(bar)处于上一级。 这两个包都位于名为 node_modules 的文件夹中,但是foo包所依赖的bar包在此却是以软链形式存在。

  • foo可以require引入bar,是因为Node.js在目录结构中查找模块,直到磁盘的根目录。 foo也以引用foo,因为它位于一个名为 node_modules 的文件夹中(是的,这是一些包会这样做)。

你信吗?

只需通过npm安装pnpm:npm install -g pnpm, 当你想安装一些东西时,用pnpm代替npm,例如: pnpm i foo

你也可以在pnpm GitHub repo中阅读更多详情。你也可以在推特上关注pnpm 或在pnpm Gitter 聊天室寻求帮助