介绍
npm、yarn和pnpm是目前主流的js包管理工具,本文将对这三种工具进行简要分析。
npm介绍
npm是Node默认的包管理工具,可以根据项目中的package.json文件自动解析和安装项目依赖。主要的步骤是:
- 读取package.json文件,构建依赖树
- 判断当前包是否有缓存判断是否过期(304判断),没过期则使用缓存
- 如果过期则通过config的源下载对应的tar包并解压到本地缓存目录下
- 将依赖从缓存中拷贝到项目下的node_modules目录下
早期版本的npm(npm1, npm2)采用递归方式安装,这会导致很多问题。比如A依赖B和C,B又依赖C,则在递归安装的过程中会在A的node_modules目录中安装B和C,B的node_modules目录中也会安装C,这样C就被重复安装了2次。
当依赖树更加复杂后,这会导致大量的重复安装,既费时又占用额外空间。此外在windows系统下,嵌套的目录层级过多会导致路径名称过长从而无法删除node_modules目录。
后来npm为了解决以上问题采用了扁平化算法,在构建依赖树之后将其扁平化。还是刚才的例子:A依赖B和C,B又依赖C,在扁平化之后B和C就会直接放到A的node_modules目录下。当B需要访问C时会先在自己的node_modules目录下找,如果找不到,则会到父级目录的node_modules下寻找,一直到根目录。这样即使B中没有安装C,也可以访问到父级目录下的C。
但是这种方式依旧存在问题,比如最终结果的不确定性。比如项目依赖A和B两个包,A依赖C1.0,B依赖C2.0,这样构建的结果是
my-app/node_modules{
--A
--B
--B/node_modules{
----C2.0
--}
--C1.0
}
还是
my-app/node_modules{
--A/node_modules{
----C1.0
--}
--B
--C2.0
}
其实是不确定的,取决于A和B在package.json中声明的顺序,如果A先声明就是第一种,否则就是第二种。
yarn介绍
yarn出现时解决了上面的问题,通过使用yarn.lock文件确定最终依赖结构的稳定性。后来的版本中npm也采用了package-lock.json来确保这种稳定性。在lock文件中记录了每个依赖包的精确版本以及目录结构,从而减少由于包版本不一致导致的bug。
除此之外,yarn优化了缓存的利用和网络请求。如果缓存中下载过同样的包,yarn可以直接从缓存中拷贝到项目目录下,而无需通过网络下载。即使通过网络下载时,yarn的fetch队列能够更充分地利用网络资源,并且有着自动的超时重试机制,不会因为某个包无法获取而导致整个安装卡死。
但是无论是yarn还是npm都无法解决这些问题:
- 扁平化算法过于复杂,耗时较长
- 扁平化结构带来的非法访问
- 将包从缓存拷贝到目录下的过程有大量I/O操作,耗费时间且每个项目都会占据一份空间
pnpm介绍
pnpm解决以上问题的方案是使用硬连接。使用pnpm时,在同一个系统中所有相同的包是会被下载一次。各个项目在安装依赖时,会通过硬连接的方式连接到本机上的pnpm的store目录中。也就是说,项目中的node_modules下不再是真实的文件,而是硬连接。这样的好处显而易见,使用硬连接代替拷贝操作省去了I/O操作,同时节省了空间。
同时pnpm无需复杂的扁平化算法,在构建依赖结构时pnpm会在每个包的node_modules中包含自己本身,相当于把包本身和它的依赖放在同一级目录下,这样就允许包依赖自己,同时避免循环结构。如下图所示:
虚线代表使用硬连接,实线代表真实的目录。可以看到在处理相同的依赖时,相同的依赖通过硬连接相连,最终连接到pnpm的store中,也就是说目录层级不会随着依赖树的的增长而增加。
同时性能上也得到了大幅提升:
可以看到大多数场景下,pnpm的表现都优于其他几种工具
另外,在npm/yarn的扁平化结构中,如果foo依赖A和B,bar依赖C和D,在经过扁平化之后,A,B,C,D都会在同一级目录下。如果foo中引入C或者D也可以成功访问,即使foo的依赖中没有C和D,这就会存在潜在的安全问题。在pnpm的结构中由于依赖树的层级没有被扁平化,非法访问的情况自然也就不存在了。
总结
本文仅作为学习过程中的笔记,有任何错误或理解不当之处欢迎指出
参考资料