对于前端来说,javascript的包管理工具,主流主要是pnpm,npm,yarn,今天来对比性的讨论一下三者的区别。
这三个包管理工具说不上有关系,但是可以说颇有渊源,他们发布的时间如下:
其实用时间也大致可以看出从npm >yarn>pnpm的时间趋势,而后者之所以能够在前者已经占有市场的基础上,有自己独特的地方,同时更好的解决了前者未解决的问题.
大家可以想象一下,在上述的包管理之前没有之时,开发者是如何进行使用别人造的轮子呢,比如如果想使用vue,那我们只能到vue的官方网站,去下载对应的版本,这样的大家设想一下场景:
- 一些包不好找,可能会从其他一些下载平台下载对应的js文件,至于有没有被人加点啥就不知道了
- 发现一些包需要升级,但是升级的包,但是不知道升级哪个版本
- 如果系统依赖的包特别多的情况下,下载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,但是项目中怎么直接多了这么多依赖,这样的设计,虽然便捷的解决了重复安装的问题,一定程度减小了包的大小,但是也带来了一个问题 幽灵依赖,
上面可以看到:
- 开始安装express,如果没有锁定具体的版本,采用了^或者~,上图的例子是
express: ^1.0.0,此时如果按照上述平铺的方式,我们可以直接在项目中使用url的库,而不用安装他 - 此时如果express官方升级,去除了
url的库 - 我们此时如果不知情,直接在生产流水线执行打包,过程中执行了
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大致做了哪些事情:
- 解析依赖树,这里包括着确定版本,解决冲突等
- 下载包文件:从远程仓库中下载
- 处理缓存和冲突,,写入到
node_modules目录
这里的流程说起来很简化,大家也不必较真,npm是非并发式的进行安装,而且需要解析完整个依赖树时才能进行安装,所以相对来说速度比较慢
大家可以参考下图(这个图也是老演员了,这里推荐一下百度搜索图片之后的AI修复功能,确实挺方便,这个图片搜的都是很模糊的,修复之后看起来还可以吧)
正是由于npm的这些问题,后来yarn才提出并行下载和本地缓存机制,但是从上图可以看到,在重复安装和使用cache时,yarn确实快于npm
这里就不得不说到pnpm,从上图可以看到,pnpm在不使用cache的场景下都快于前两者,原因pnpm官方网站上也有解释:
可以看到pnpm并没有等待整个依赖树解析完成就开始下载了,这也是和pnpm的包组织设计有关,因为node_modules下面都是对于全局store的引用,那我就都可以并发的下载到store里,最后在去做引用,确实大大的提高了速度
3. 磁盘空间
这里我也有个疑问,且听我说来
之前的版本我们可以忽略不计,按照最新的npm和yarn都采用平铺的方式来组织node_modules目录,可以理解为node_modules目录大小是一致的,但是yarn由于多了本地缓存,所以对于磁盘的占用应该是高于npm的,特别是对于多个项目
问题是查阅好多文档,都只是描述说
yarn使用本地缓存,提升了下载的效率,对于是否节省了空间没有明确提到,我自己查看项目理解的是:yarn是使用本地缓存,下载时如果本地缓存存在,就直接copy一份到项目的node_modules中,所以第二次安装速度快,但是磁盘占用应该是高于npm,这里也欢迎明白的同学评论指正
而针对pnpm,单个包的版本,磁盘只会存储一份,其他的均是引用,所以对于磁盘的节省不言而喻了
4. 问题
对于这三者的比较远远不止上面的三块,但是对于初步的了解够用了
而且对于包管理工具来说,大家也都在互相的进行借鉴和完善,而且在后续的版本中,也都有了不同工具之间的迁移和支持,所以最近的这几个管理工具,对于基础的使用,基本均能满足,至于孰优孰劣,就凭大家喜好啦
参考文档:
1.benchmarks-of-javascript-package-managers
2.区别