从2017年pnpm的诞生,到现在各公司都在去npm、yarn化,pnpm正在陆续接管各大前端项目的依赖包安装。原因在于她使用软链接和硬链接的方式不仅提高了安装速度还节约了磁盘空间,同时避免了依赖分身和幽灵依赖等问题。
这篇文章先对比一下npm、yarn、pnpm的区别,再了解一下硬链接和软链接的应用。开始吧。
npm
老牌包管理工具npm的成功主要原因是Node的横行,引入了package.json文件,将所有的依赖都添加到了package.json中,比如运行 npm install --save lodash,以下几行将会添加到package.json中。
"dependencies": {
"lodash": "^4.17.21"
}
lodash版本前的^符号意思是安装主版本等于4的任意一个版本即可。所以两个人不同时间的安装是会出现不同版本的情况的。虽然说小版本升级一般会兼容上一个版本,但是升级翻车的事情也是屡见不鲜的。要想版本完全一致,lock版本或使用docker。说回来,npm的包大多都是依赖于其他npm包的,这会导致嵌套依赖关系并增加无法匹配想要版本的几率。
虽然说我们可以锁住安装包的版本,但是包的依赖取决于其本身的package.json的写法。我们也无法控制。为了解决这个问题,npm提供了shrinkwrap命令,此命令会生成一个npm-shrinkwrap.json的文件,用来记录所有包及其依赖包的确切版本。 这个命令很好用,但是知名度好像不高。问过身边好几个同事,都不清楚这个命令。如此好用的命令也是只能保证我们在版本问题上不出问题。对于包内容的变更还是不会检测的。
现在npm的版本早就解决了npm2上嵌套依赖树的问题。因为这个结构在一些场景下会变得很长,这对于Unix操作系统来说是个问题。因为很多程序是无法处理超过260个字符的文件路径的。
yarn
2016年yarn的出现主要解决了npm的两个主要问题,
- 语义控制导致npm安装不确定的问题。
- npm安装包依赖嵌套的的问题(虽然npm3及时的解决了这个问题)。
上面说npm可以通过shrinkwrap
命令来生成npm-shrinkwrap.json的文件来控制安装包及其依赖包的版本问题。而yarn是可以直接生成yarn.lock文件的,默认生成的。 高手过招,胜负往往就在半招之间。人的惰性让默认选项更胜一筹。yarn 不仅可以控制安装包版本,还可以对安装包内容进行校验。
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
intergrity 是对resolved 下载下来的文件进行完整性校验,如果出现了diff,就说明下载链接对应的文件被修改过。安装的时候就会给出提示,安全性进一步增强了。
另外在速度上面,yarn的并行下载也会让其下载速度快很多。
pnpm
而pnpm算是站在了巨人的肩膀上,当然这个巨人不是npm和yarn,而是软链接和硬链接。
pnpm相比于前辈们最大的不同就是快和空间管理。
在快方面,官方的图有些量化的依据。
pnpm能有这么快的安装速度得益于她的包管理机制,速度上能快2~3呗。稍微大一点的项目,在部署环节install的时候能减少不少时间。
非扁平的结构
yarn和npm安装依赖包,会在node_modules下都平铺出来。比如你install一个包 express,但是你的node_modules下会有很多包。
这个时候会有一些问题
- 幽灵依赖
从目前的包引用方式来说,inport的时候我们会从node_modules的文件夹中寻找,按照上面的图中所示,如果我们在package.json中没有accepts,其实我们也是可以引用到的,因为她确实存在,这时候我们访问的就是未申明npm包,如果某一天express不再依赖accepts,这个时候项目就会有依赖缺失的问题。 我们把这种主包依赖的子包,未被申明而在项目中使用,我们称之为 幽灵依赖。
-
包版本的不确定性
这个很好理解,如果A、B两个主包都依赖accepts包,但是A依赖accepts@1.0,B依赖accepts@2.0 ,那node_modules下的扁平结构是展示1.0 还是 2.0 呢?目前的方式是谁后安装的谁就显示。 这种不确定性在开发中引起的问题也不在少数 「别人用这个包可以解决这个问题,但是我安装这个包就不能解决」,往往就是这个原因导致的。
-
依赖重复安装
这个也很好理解,AB都依赖accepts,依赖不同的版本,无论node_modules的顶层提升了哪个版本,这个包都是会被安装两次的。
另外,我们都会发现一个问题。比如我们一般都会将公司的项目clone到一个以公司命名的文件夹中,随着项目的增多,和开发的增加。你会发现这个文件夹变的巨大无比,其中我们的业务代码只占其中30%左右,剩余的都是node_modules中的依赖包。而这其中的依赖包,有很大比例都是一样的。这也会疯狂的占据我们的磁盘空间。
pnpm的包管理方式是通过建立链接的方式来完成快速安装和防止重复安装(以减少磁盘空间)的问题。
其实说到安装也不是说非要从哪里下载下来,只要有个产物可以供我消费,无论我是安装下来还是可以从网上直接用(cdn)或者源文件在其他地方我link一下,都可以,达到供我消费的目的就可以。
说真的,在业务开发当中,我们主要关心的是业务代码,而安装的依赖,一般我们都不怎么刻意关心。
话说回来,我们可以先看一下pnpm的产物。
我们可以看到node_modules下面很干净,只有express和.pnpm文件夹,相比于npm和yarn的产物,清爽了不少。
我们看一下pnpm官方对这一现象的图示说明
顶级外层来看,格式很清晰,.pnpm中也是嵌套的。这是因为pnpm的node_modules布局使用的是符号链接来创建依赖关系的嵌套结构。.pnpm内部的每个包中的每个文件都是只用硬链接指向了.pnpm store 中的文件。
这样的好处就是会让我们的node_modules很清晰,内部的包可以和package.json中的依赖对应起来,一目了然,我们安装什么里面就有什么。
这样幽灵依赖的问题就解决了,包版本不确定性的问题也就解决了。毕竟顶层就只有我们手动安装的包,其他依赖包都收在.pnpm中。这样无论是哪个版本都会平铺在这里供你消费。 这个平铺的方式就是通过链接的形式进行引用。
好,我们知道了pnpm是通过hard link(硬链接)的方式来提升能力。那我们要聊一下她的使用和应用了
hard link
来自维基百科的解释:硬链接(英语:hard link)是计算机文件系统中的多个文件平等地共享同一个文件存储单元(如MFT条目、inode)。硬链接必须在同一个文件系统中;一般用户权限下的硬链接只能用于文件,不能用于目录,因为其父目录就有歧义了。删除一个文件名字后,还可以用其它名字继续访问该文件。硬链接只能用于同一个文件系统(对于NTFS是限制于同一个分区)。不能用于不存在的文件。
要想进一步了解硬链接我们需要从inode说起,大概的我们用大白话讲一下。
一个文件存储在硬盘上,硬盘最小的存储单位叫做扇区(0.5kb),系统读取的时候会一次性读8个扇区,我们称之为块。我们还需要找个地方用来存储这个文件的基本信息,这个东西我们就叫做inode。
举个例子,我会把收纳的东西放到一个小盒子里,然后我会将8个小盒子放到抽屉里。我找东西的时候是会拉开抽屉同时看到8个小盒子的。这样我找起来也会快一点。但是更前提是我得先找到抽屉的把手,然后拉开抽屉。 这样盒子对应的是扇区,抽屉对应着块,抽屉的把手对应着inode。
inode的基本信息有很多,包括
- 文件的字节数
- 文件拥有者的User ID
- 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
- 链接数,即有多少文件名指向这个inode
- 文件数据block的位置
使用 stat xxx.txt 命令 可以查看inode信息。
每个inode都有一个号码,这个在操作系统中是独一无二的。操作系统就是用过这个号码找到文件的。所以说,我们表面上是打开一个文件,实际上是系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。
使用ls -i命令,可以看到文件名对应的inode号码: ls -i xxx.txt
同样的命令可以作用于文件夹,因为在Unix/Linux系统中,文件夹(目录)也是一种文件。
一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。
这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。
使用 ln 命令可以创建硬链接:
ln 源文件 目标文件
硬链接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬链接到重要文件,起到防止“误删”的功能。
举个例子:
我们创建一个文件 a.txt,touch a.txt,然后 vi a.txt,我们输入点字符,this is a。
这个时候我们执行 ln a.txt b.txt。
这个时候再执行 ls。我们会发现。
(base) ➜ ls
a.txt b.txt
有两个文件。
然后我们 vi b.txt ,发现了一行 this is a。说明现在 a就是b,b就是a。🐶
然后我们 rm a.txt,这个时候发现就剩下 b.txt。 但是我们打开 b.txt , 还是可以看到this is a。
所以说硬链接是可以防止误删的。一点毛病没有。这个时候有人就会问了,那我怎么才能都删了呢?好问题,自古评论区人才济济,这个就交给大家讨论了。
具体还有哪些应用呢?
文件分类
比如我有一个文件夹,存储了10 个G的照片。这些照片中的人物、拍照地点、拍照时间都是不一样的。
现在,我既想根据照片中的人物进行分类,也想根据拍照地点进行分类,还想根据拍照时间进行分类,那该怎么办?
因为一张照片可能同时属于多个不同的分类,难道每个分类中都复制一张照片?这样也太浪费硬盘空间了!
解决方案是:
所有的照片仍旧放在一个总的文件夹中,然后创建不同的分类文件夹,在每个分类文件夹中,创建硬链接到目标照片文件。
这样的话,不仅对照片进行了分类,而且一点都不占用硬盘空间。
文件多人共享
当很多人同时对同一个文件进行维护的时候,如果大家都直接操作这个文件,万一不小心把文件删除了,大家就都玩完了!
此时,可以在每个人自己的私人目录中,创建一个硬链接。
每次只需要对这个硬链接文件进行操作,所有的改动会自动同步到目标文件中。
由于每个人都是操作硬链接文件,即使不小心删除了,也不会导致文件的丢失。
因为删除硬链接文件,仅仅是把该文件的 inode 节点中的 links 值减 1 而已,只要不为 0,就不会真正的删除文件。
文件备份
一些小伙伴有定期备份文件、清理文件的好习惯。
在备份的时候,如果是实实在在的拷贝一份,那真的是太浪费磁盘空间,特别是对于我这种只有 256G 硬盘空间的笔记本。
此时,就可以利用硬链接功能,既实现文件备份的目的,又节省了大量的硬盘空间,一举两得!
很多备份工具利用的就是硬链接的功能,包括 git 工具,当克隆本地的一个仓库时,执行 clone 指令:
git clone --reference <repository>
git 并不会把仓库中的所有文件拷贝到本地,而仅仅是创建文件的硬链接,几乎是零拷贝!
symbolic link
除了硬链接以外,还有一种特殊情况。符号链接也成为软链接。
文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。
这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:"No such file or directory"。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。
ln -s命令可以创建软链接。
ln -s 源文件 目标文件
灵活切换不同版本的目标程序
在开发的过程中,对于同一个工具软件,可能要安装多个不同的版本,例如:Python2 和 Python3, JDK8 和 JDK9 等等。
此时就可以通过软链接来指定当前使用哪个版本。例如在我的电脑中:
$ ll -l /usr/bin/python*
lrwxrwxrwx 1 root root 9 12月 31 08:19 /usr/bin/python -> python2.7*
lrwxrwxrwx 1 root root 9 12月 31 08:19 /usr/bin/python2 -> python2.7*
-rwxr-xr-x 1 root root 3492624 3月 2 04:47 /usr/bin/python2.7*
lrwxrwxrwx 1 root root 9 12月 31 08:19 /usr/bin/python3 -> python3.5*
-rwxr-xr-x 2 root root 4456208 1月 27 02:48 /usr/bin/python3.5*
当在终端窗口中输入:python 时,启动的是 python2.7 版本。
如果有一天我需要使用 python3.5 版本,只需要把软链接 python 指向 python3.5 即可。
快捷方式
利用软链接的快捷方式功能就比较好理解了,想一想:我们为什么在 Windows 的桌面上创建很多软件的快捷方式啊?
在 Linux 中同样如此!
比如:最近一段时间的工作,每次都要打开一个路径很深的文件。
如果在资源管理器中,一层一层的点击鼠标,是不是比较浪费时间。
此时,就可以在桌面上创建一个软链接,每次直接双击就打开所链接的目标文件了。
总结
pnpm后来者居上,让我们有了解决npm包的新方式。使用pnpm的同时我们更应该了解一下背后基础技术的原理和方式。有可以会打开新的视野思路。就好比张无忌练乾坤大挪移那么快那么强,离不开九阳真经的背后加持。那我们如果掌握了九阳真经那我们就不止于乾坤大挪移了,太极拳剑、圣火令武功等也是可以搞搞的。
参考
https://zhuanlan.zhihu.com/p/404784010
https://zhuanlan.zhihu.com/p/419399115
https://www.51cto.com/article/667057.html
https://www.pnpm.cn/symlinked-node-modules-structure
https://zhuanlan.zhihu.com/p/442133074
https://juejin.cn/post/7047429181021356062
文章评论