Pnpm、npm和yarn三者对比


highlight: a11y-dark

对于前端来说,javascript的包管理工具,主流主要是pnpm,npm,yarn,今天来对比性的讨论一下三者的区别。

这三个包管理工具说不上有关系,但是可以说颇有渊源,他们发布的时间如下:

其实用时间也大致可以看出从npm >yarn>pnpm的时间趋势,而后者之所以能够在前者已经占有市场的基础上,有自己独特的地方,同时更好的解决了前者未解决的问题.

大家可以想象一下,在上述的包管理之前没有之时,开发者是如何进行使用别人造的轮子呢,比如如果想使用vue,那我们只能到vue的官方网站,去下载对应的版本,这样的大家设想一下场景:

  1. 一些包不好找,可能会从其他一些下载平台下载对应的js文件,至于有没有被人加点啥就不知道了
  2. 发现一些包需要升级,但是升级的包,但是不知道升级哪个版本
  3. 如果系统依赖的包特别多的情况下,下载js库都是一件令人头大的事

所以有了这些痛点,所以当年的先驱们设计出了npm来统一管理这些公共包,那这个包管理工具包括哪些部分呢:

所以对于包管理工具基本都是由这三部分构成:

  • the Command Line Interface (CLI):命令行工具,也是大多数同学使用的方式,可以查看,发布等操作
  • the registry: 是个大型的公共的javascript库的数据库
  • the website:可以查看包,管理自己的包

知道了基本的结构,我们接下来从4个方面来对pnpm,npm,yarn进行对比:

1. node_modules目录结构

js的包管理工具将根据package.json的配置,将远程的依赖包数据都下载到本地的node_modules的目录,但是对比内部的目录结构处理略有不同,这里我们要展开说一下。

1.1 npm < 3.x时代

在npm诞生之初,npm就是最好的包管理工具,应该还有一方面原因是node社区并不是特别大,很多包也不涉及到深层次的循环依赖,通过查看npm和node版本对应关系,我们特意安装了2.15版本的npm,只安装了express(后续也用这个包进行对比)查看一下目录:

- node_modules
    - .bin
    - express
        - node_modules
            - accepts
            - array-flatten
            - ...
            - vary

此时可以看到npm将包安装到node_modules,同时express的依赖像树一样直接一层一层往下延伸。这里也看到好多文章提出这样设计的问题:

  • 层级比较深,如果依赖A->B->C->D->E->F->G.. 这样就会导致包的目录结构特别深
  • 同时如果A依赖了B,同时项目根也依赖了B,但是B依然会安装两遍,这样会导致依赖的包会比较大,占据磁盘空间

但是这里也要对当时npm的正名一下,因为每一个设计都当时的环境有关系,在当时的环境包的依赖都不算大,而且整个npm的社区还没形成,都还在自己造轮子阶段,所以也很少会出现依赖特别深的情况,所以在当时这样的设计也满足的需求。同时现在来看,这样的设计我个人感觉有种大道至简的感觉,结构清晰,个人看法。

1.2 npm > 3.x 时代

由于上面的问题,也是社区繁荣发展的必然,需要有新的设计来满足大家的需求,所以提出了平铺的设计,还以上面的为例:

- node_modules
    - .bin
    - accepts
    - array-flatten
    - express
        - lib
        - package.json
    - ...
    - vary
    - .package-lock.json

此时可以看到包已经平铺到项目中,这也是后续延续到当前版本的一个大致结构

1.3 yarn

yarn在推出之初已经按照平铺的结构进行组织node_modules目录:

- node_modules
    - .bin
    - accepts
    - array-flatten
    - express
        - lib
        - package.json
    - ...
    - vary
    - .yarn-integrity

基本和npm >3 之后是一致的了

1.4 pnpm

看上面的目录结构,不知道大家会不会有疑惑,为什么我只使用了express,但是项目中怎么直接多了这么多依赖,这样的设计,虽然便捷的解决了重复安装的问题,一定程度减小了包的大小,但是也带来了一个问题 幽灵依赖

上面可以看到:

  1. 开始安装express,如果没有锁定具体的版本,采用了^或者~,上图的例子是 express: ^1.0.0,此时如果按照上述平铺的方式,我们可以直接在项目中使用url的库,而不用安装他
  2. 此时如果express官方升级,去除了url的库
  3. 我们此时如果不知情,直接在生产流水线执行打包,过程中执行了npm install,此时的打包产出就会在使用url的代码处报错

所以此时才有了pnpm的设计,在最外层只会出现需要express,而express依赖的包只出现在.pnpm中(如下图),此时如果用上面的例子来说,没有安装url时直接使用,肯定是会报错的

- node_modules
    - .pnpm
        - accepts
        - array-flatten
        - express
            - node_modules
                - accepts(软链)
                - array-flatten(软链)
                - ...
                - vary(软链)
        - ...
        - vary
    - express
        - lib
        - package.json

2. 安装速度

首先能确认的是npm应该是最慢的,,因为我们要知道npm install大致做了哪些事情:

  1. 解析依赖树,这里包括着确定版本,解决冲突等
  2. 下载包文件:从远程仓库中下载
  3. 处理缓存和冲突,,写入到node_modules目录

这里的流程说起来很简化,大家也不必较真,npm是非并发式的进行安装,而且需要解析完整个依赖树时才能进行安装,所以相对来说速度比较慢

大家可以参考下图(这个图也是老演员了,这里推荐一下百度搜索图片之后的AI修复功能,确实挺方便,这个图片搜的都是很模糊的,修复之后看起来还可以吧)

正是由于npm的这些问题,后来yarn才提出并行下载和本地缓存机制,但是从上图可以看到,在重复安装和使用cache时,yarn确实快于npm

这里就不得不说到pnpm,从上图可以看到,pnpm在不使用cache的场景下都快于前两者,原因pnpm官方网站上也有解释:

image.png

可以看到pnpm并没有等待整个依赖树解析完成就开始下载了,这也是和pnpm的包组织设计有关,因为node_modules下面都是对于全局store的引用,那我就都可以并发的下载到store里,最后在去做引用,确实大大的提高了速度

3. 磁盘空间

这里我也有个疑问,且听我说来

之前的版本我们可以忽略不计,按照最新的npmyarn都采用平铺的方式来组织node_modules目录,可以理解为node_modules目录大小是一致的,但是yarn由于多了本地缓存,所以对于磁盘的占用应该是高于npm的,特别是对于多个项目

问题是查阅好多文档,都只是描述说yarn使用本地缓存,提升了下载的效率,对于是否节省了空间没有明确提到,我自己查看项目理解的是:yarn是使用本地缓存,下载时如果本地缓存存在,就直接copy一份到项目的node_modules中,所以第二次安装速度快,但是磁盘占用应该是高于npm,这里也欢迎明白的同学评论指正

而针对pnpm,单个包的版本,磁盘只会存储一份,其他的均是引用,所以对于磁盘的节省不言而喻了

4. 问题

对于这三者的比较远远不止上面的三块,但是对于初步的了解够用了

而且对于包管理工具来说,大家也都在互相的进行借鉴和完善,而且在后续的版本中,也都有了不同工具之间的迁移和支持,所以最近的这几个管理工具,对于基础的使用,基本均能满足,至于孰优孰劣,就凭大家喜好啦

参考文档:

1.benchmarks-of-javascript-package-managers

2.区别


这是一个从 https://juejin.cn/post/7369038974549360655 下的原始话题分离的讨论话题