npm yarn pnpm对比

198 阅读6分钟

pnpm 官网

npm2

node_modules 是嵌套的

image.png

问题一:多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。

问题二:致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。

yarn和npm3

yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?

铺平。使用依赖提升,所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。

image.png

此时大部分包都不嵌套了,但是还是有的会嵌套:

因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。

image.png

npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:

当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。

yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?

并不是,扁平化的方案也有相应的问题。

问题一幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。

  • foo依赖bar,由于扁平化管理foo和bar会变成存在于同一层目录下。
  • dependencies 中依赖foo但不依赖bar,却可以require进来bar:因为bar是一个幽灵依赖。
  • 若某一天随着foo包升级,导致它不再依赖bar,那么在项目引用bar就会直接报错,因为根本没安装过这个包

问题二:就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。(提升哪个取决于用户的安装顺序)(如果有 package.json 变更,本地需要删除 node_modules 重新 install,否则可能会导致生产环境与开发环境 node_modules 结构不同,代码无法正常运行。)

pnpm

外部依赖使用软连接,.pnpm内铺平,硬链接到全局store

注意:

node_modules下仅存放首层依赖(如express),即项目实际需要的依赖,次级依赖(express的依赖)则在.pnpm内平铺

次级依赖除了在.pnpm内平铺外,还要在相应首层依赖下的node_moudles中平铺,如.pnpm/express@4.17.1/node_modules/xxx,然后软连接到 .pnpm/xxx@1.1.1/node_modules/,最后硬链接到全局store

使用pnpm安装,node_modules 不再是扁平化了。

.pnpm中铺平全部依赖,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后包和包之间的依赖关系是通过软链接组织的。

node_modules
├── f     **软链接到.pnpm下的f**
└── .pnpm
       └── g@1.0.0   **硬链接到store**
       └── f@1.0.0
             └── node_modules
                   └── g  **g是f的依赖包,软链接到.pnpm下的g@1.0.0 **
                   └── f -> <store>/f     **硬链接到store**

也就是说,外面的依赖包不再以实体文件的形式存在,而是创建了一个软链接(有点像windows系统的快捷方式)指向.pnpm内的指定依赖,用到该依赖包时用硬链接去store里面取

官方给了一张原理图,配合着看一下就明白了:

bar@1.0.0为项目实际需要的依赖包,软连接到.pnpm内部平铺的bar包的位置,注意foo@1.0.0为bar@1.0.0依赖包的依赖,作为次级包与bar放在一层上,再通过软连接到.pnpm下平铺的foo位置。

pnpm monorepos模式

接下来讲下如何用pnpm来开发和管理组件,以及这样做有什么好处?

优势一:快

由于有了hard link和全局store的加持,在开发环境中编码,热更新的开发服务响应是非常快的,给开发者有良好的体验环境。

优势二:管理方便

多个组件间的调用依赖简单,举个例子:在1个pnpm monorepos工程里面有3个组件,其中playground只提供给开发者本地调试用,small-color-uiutils作为发布包,并且small-color-ui依赖utils

.packages

├── playground #本地调试后台,不发布

├── small-color-ui #代码引用utils

└── utils #基础包

我们只需要把3个包在pnpm workspace注册,便能像引用远程组件那样去引入,而且支持实时本地调试。这个功能像是pnpm自动帮我们做好了npm link

优势三:解决monorepos的结构性依赖痛点

幽灵依赖

常见于yarn体系下,例如依赖里面有个包名叫 foofoo 里面依赖了 bar,经过 yarn 的扁平化处理,会把依赖foobarnode_modules 同一层级目录下。那么根据 nodejs 的寻径原理,用户能 requirefoo,同样也能 requirebar

这样的bar就是一个幽灵依赖,它有什么问题呢?直到某一天随着foo包升级,导致它不再依赖bar,那么在项目引用bar就会直接报错,因为根本没安装过这个包。

但这种情况不会发生在pnpm中。上面讲过pnpmnode_modules的结构是包名 + 内部依赖 + 版本信息的序列目录列表,在项目也根本没法直接require bar

依赖分身

就是说不同依赖中的子依赖同一个包,而且这个包版本还不一致,导致项目重复安装子依赖包,致增加依赖的维护成本。

pnpm体系下,由于所有依赖都打平到全局store里面了,所以不同版本的依赖只会安装一次,足以被整个项目所用。

当然,假如子依赖的版本不一致,pnpm还是会安装多次的,但是所有父依赖包的引用地址只会指向一处,这也弥补性能和空间上的性能缺陷。

总结

pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因(优点):

  • npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。
  • npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。
  • pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。