浅谈pnpm & (软链接 与 硬链接)

本文介绍了前端包管理工具pnpm,对比了npm和yarn的优缺点,深入探讨了硬链接和软链接的概念。pnpm利用这两种链接优化了依赖管理,减少了磁盘占用,提高了性能。通过实战安装Vue的例子,展示了pnpm如何通过软链接解决依赖问题。文章还讨论了从yarn迁移到pnpm的过程及可能遇到的问题,并提供了迁移命令。
摘要由CSDN通过智能技术生成

概述:

前端常见包管理工具:yarn、npm、pnpm,本文主要浅谈三种包管理工具的优劣与pnpm的原理与使用。
pnpm官网:https://pnpm.io/zh/motivation

一、npm、yarn遇到的挑战

1.1 npm v1/ npm v2:嵌套结构
 node_modules
└─ a
|   ├─ index.js
|   ├─ package.json
|   └─ node_modules
|      └─ b
|        ├─ index.js
|        └─ package.json
└─ c
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ b
         ├─ index.js
         └─ package.json
  • 优点:
    • node_modules 结构是可预测和干净的,因为 node_modules 中的每个依赖项都有自己的 node_modules 以及所包含依赖关系的 package.json 文件。
  • 问题:
    • 依赖无法被共用:比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。
    • 依赖层级太深,导致文件路径过长。
    • 下载大量重复依赖:如上述node_modules目录,b包会在a包、c包两者的node_modules中安装,即重复安装。
1.2. npm v3/ yarn v1:依赖扁平化
 node_modules
├─ a
|  ├─ index.js
|  └─ package.json
└─ b
|  ├─ index.js
|  └─ package.json
└─ c
   ├─ index.js
   └─ package.json
  • 优点:
    • 依赖目录平铺,解决层级太深问题
    • 减少重复依赖下载,相同依赖只提升某一版本(仍有大量重复依赖的下载)
  • 问题:
    • Phatom dependencies(幽灵依赖):模块可以访问package.json没有注册的包,开发者也能引入。

    • 扁平化算法本身的复杂性很高,耗时较长
      在这里插入图片描述

    • 依赖结构的不确定:如上述a、c依赖的b包版本不同,那么b包哪个版本会被提升呢?
      在这里插入图片描述
      在这里插入图片描述

      • 答案是:都有可能。取决于 a 和 c 在 package.json中的位置,如果 c声明在前面,那么就是前面的结构(b@1.1.0被提升),否则是后面的结构(b@1.0.0被提升)。
      • 这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。尽管如此,npm/yarn 本身还是存在扁平化算法复杂和package 非法访问的问题,影响性能和安全。

二、pnpm浅析

2.1. 操作系统相关知识(文件系统、硬链接、符号链接)

pnpm使用符号链接硬链接来构建node_modules目录,在这之前先让我们了解一下操作系统的相关知识。

2.1.1 文件的本质

扇区(sector)
硬盘的最小存储单位叫「扇区」(sector,每个扇区存储512bit)
操作系统一次读一个扇区效率太低,所以会一次读多个扇区
(block)
多个扇区组成一个块,最常见的大小为4kb(即由8个扇区组成)
块是文件存取的最小单元,文件数据都存储在块中
索引节点(inode)
存储文件元信息,比如文件的创建者、文件的创建日期、文件大小等
Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。
打开文件时,系统首先找到文件的inode号码,然后通过inode号码获取inode信息。然后根据inode中的文件数据所在block读取数据。

在Linux系统中,内核为每一个新创建的文件分配一个inode,每个文件都有一个惟一的inode号,我们可以将inode简单理解成一个指针,它永远指向本文件的具体存储位置。文件属性保存在inode里,在访问文件时,inode被复制到内存,从而实现文件的快速访问,系统是通过inode来定位每一个文件。

inode展开来讲又是一个单独的章节,本文下面简化了文件的inode原理,便于直观理解。
在操作系统中,文件实际上是一个指针,只不过它指向的不是内存地址,而是一个外部存储地址(这里的外部存储可以是硬盘、U盘等)
在这里插入图片描述
当我们删除文件时,删除的实际上是指针,因此,无论删除多么大的文件,速度都非常快。像我们的U盘、硬盘里的文件虽然说看起来已经删除了,但是其实数据恢复公司是可以恢复的,因为数据还是存在的,只要删除文件后再没有存储其它文件就可以恢复,所以真正删除一个文件就是疯狂存疯狂删。(陈mou希:***??
在这里插入图片描述

2.1.2 文件的拷贝

复制一个文件,是将该文件指针指向的内容进行复制,然后产生一个新文件指向新的内容。
在这里插入图片描述

2.1.3 硬链接(hard link)

硬链接实际上是一个指针,指向源文件的inode,系统并不为它重新分配inode。硬连接不会建产新的inode,硬连接不管有多少个,都指向的是同一个inode节点,只是新建一个hard link会把结点连接数增加,只要结点的连接数不是0,文件就一直存在,不管你删除的是源文件还是连接的文件。只要有一个存在,文件就存在(其实就是引用计数的概念)。当你修改源文件或者连接文件任何一个的时候,其他的文件都会做同步的修改。
硬链接文件有两个限制
1)不允许给目录创建硬链接
2)只允许在同一文件系统中的文件之间才能创建链接

硬链接的概念来自于 Unix 操作系统,它是指将一个文件A指针复制到另一个文件B指针中,文件B就是文件A的硬链接。
在这里插入图片描述
通过硬链接,不会产生额外的磁盘占用,并且两个文件都能找到相同的磁盘内容。硬链接的数量没有限制,可以为同一个文件产生多个硬链接。

2.1.4 符号链接(symbol link)

符号接最直观的解释:相当于Windows系统的快捷方式,是一个独立文件(拥有独立的inode,与源文件inode无关),实际上是特殊文件的一种, 该文件的内容是源文件的路径指针(另一个文件的位置信息),通过该链接可以访问到源文件。所以删除软链接文件对源文件无影响,但是删除源文件,软链接文件就会找不到要指向的文件(可以类比Windows上快捷方式,你点击快捷方式可以访问某个文件,但是删除快捷方式,对源文件无任何影响)。

符号链接又称为软连接,如果为某个文件或文件夹A创建符号连接B,则B指向A。
在这里插入图片描述

2.1.5 硬链接和符号链接的小结
  • 硬链接:

    • 硬链接仅能链接文件,而符号链接可以链接目录
    • 具有相同inode节点号的多个文件互为硬链接文件;
    • 删除硬链接文件或者删除源文件任意之一,文件实体并未被删除;
    • 只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除;
    • 硬链接文件是文件的另一个入口;
    • 可以通过给文件设置硬链接文件来防止重要文件被误删;
    • 创建硬链接命令 ln 源文件 硬链接文件;
    • 硬链接文件是普通文件,可以用rm删除;
    • 对于静态文件(没有进程正在调用),当硬链接数为0时文件就被删除。注意:如果有进程正在调用,则无法删除或者即使文件名被删除但空间不会释放。
  • 符号链接/软链接:

    • 软链接类似windows系统的快捷方式;
    • 软链接里面存放的是源文件的路径,指向源文件;
    • 删除源文件,软链接依然存在,但无法访问源文件内容;
    • 软链接失效时一般是白字红底闪烁;
    • 创建软链接命令 ln -s 源文件 软链接文件;
    • 软链接和源文件是不同的文件,文件类型也不同,inode号也不同;
      软链接的文件类型是“l”,可以用rm删除。

由于符号链接指向的是另一个文件或目录,当node执行符号链接下的JS文件时,会使用原始路径。比方说:我在E盘装了CFHD,在桌面创建了CFHD快捷方式,相当于是符号链接,双击快捷方式运行游戏,在运行游戏的时候是按照CFHD原始路径(E盘路径)运行的。

2.1.6 快捷方式

快捷方式类似于符号链接,是windows系统早期就支持的链接方式。它不仅仅是一个指向其他文件或目录的指针,其中还包含了各种信息:如权限、兼容性启动方式等其他各种属性,由于快捷方式是windows系统独有的,在跨平台的应用中一般不会使用。

2.1.7 node环境对硬链接和符号链接的处理
  • 硬链接:
    • 硬链接是一个实实在在的文件,node不对其做任何特殊处理,也无法区别对待,实际上,node根本无从知晓该文件是不是一个硬链接。
  • 符号链接:
    • 由于符号链接指向的是另一个文件或目录,当node执行符号链接下的JS文件时,会使用原始路径。比方说:我在E盘装了CFHD,在桌面创建了CFHD快捷方式,相当于是符号链接,双击快捷方式运行游戏,在运行游戏的时候是按照CFHD原始路径(E盘路径)运行的。
2.2. pnpm原理
2.2.1 实战安装Vue
2.2.1.1 yarn 安装 vue:

使用yarn v1来在我们的项目安装vue,执行如下命令

npm i vue -S

不难看出扁平化的依赖管理使项目的node_modules中有很多vue之外的其他依赖项。
在这里插入图片描述

2.2.1.2 pnpm 安装 vue:

使用pnpm来在我们的项目安装vue,执行如下命令

pnpm i vue -S

可以看到node_modules变得清晰了很多,只有我们想要安装的目标依赖vue和.pnpm文件夹。
在这里插入图片描述
这里可能会有一个疑问:根node_modules文件夹下只有一个vue文件夹,vue目录下也没有node_modules,那么 vue 所需的依赖不就缺失了吗?

这正是 pnpm 的巧妙之处!我们展开 .pnpm 目录,会豁然开朗,必要依赖原来在这里。
在这里插入图片描述

2.2.1.3 pnpm 的软链接

但是上面的目录结构,不符合 require() 不断向上寻找 node_modules 中依赖的规则,vue 怎么获取这些资源呢?

仔细观察,我们会发现,node_modules 的 vue 其实只是一个软链接(常用 Windows 的同学可以理解为快捷方式)。
在这里插入图片描述
在这里插入图片描述
它真正指向的位置是 .pnpm 目录中对应的包。
在这里插入图片描述
可见,.pnpm 中的 vue 才是“元神”所在,node_modules 中的只不过是“化身”。

在 vue@2.7.8/node_modules/ 目录下的 vue,自然可以从上层 node_modules 中找到 @vue/ 中的几个依赖, 我们先前担心的依赖丢失问题迎刃而解! 巧妙的是,这几个依赖其实也是软链接“化身”,他们的本体也以同样地结构安装在 .pnpm 中。

2.2.2 pnpm的硬链接

上个小节中展开介绍了pnpm的node_modules目录结构,并提到了依赖通过软链接的形式安装在了.pnpm中,那么pnpm哪里用到了硬链接呢?

pnpm 有个根目录,一般都是保存在 user/.pnpm-store 下,pnpm 通过硬链接的方式保证了相同的包不会被重复下载,比如说我们已经在 repoA 中下载过一次 express@4.17.1 版本,那我们后续在 repoB 中安装 express@4.17.1 的时候是会被复用的,具体就是 repoA 中的 express 中的文件和 repoB 中的 express 中的文件指向的是同一个 inode。

假如有10个项目使用npm或者yarn下载依赖包,那么会产生10个依赖包的副本占用磁盘。使用pnpm会将依赖包存储到统一位置,上面看到的.pnpm是虚拟存储目录,里面的文件是从统一位置做的硬链接,而和.pnpm同级的vue包是一个符号链接。

三、pnpm的依赖目录结构-小结(简洁版)

上述说了那么多,没有看懂?没有关系,下面通过简单的模拟图给你简单的答案~
假设有两个依赖:father、son,father中require了son
在这里插入图片描述
使用pnpm安装依赖时,大致分为如下几个步骤

  1. 通过package.json查询依赖关系,分析出最终要安装的包:father和son

  2. .pnpm store查看father和son是否已经有缓存,如果没有,下载到缓存中,如果有,则进入下一步。

  3. 在项目根目录中创建 node_modules 目录,并对目录进行结构初始化。
    3.1 从缓存的对应包中使用硬链接放置文件到相应包代码目录中
    3.2 使用符号链接,将每个包的直接依赖放置到自己的目录中(保证father可以直接读取到它所依赖的包)
    3.3 在工程的node_modules目录中使用符号链接,放置直接依赖
    在这里插入图片描述
    上述的目录结构图,大家可能一个疑问,.pnpm下面的node_modules目录是做什么用的呢?依赖包不都在.pnpm目录下以树型结构展开了么,比如father引用了son,也能直接在当前的node_modules目录找到,为什么上层还有一个node_modules目录呢?

    这里笔者也只是大概分析猜测了一个答案:上层的node_modules应该是为了node寻找依赖的规则:非核心模块逐层向上查找。上述的图解是比较简单的case,笔者在大型项目中尝试pnpm安装依赖后,当依赖层级过深时,就不会将子依赖展开在当前的node_modules目录下,比如下方lerna的一个依赖包:dedent,就没有在当前的node_modules中展开,而是出现在了.pmpm下面的node_modules中。
    在这里插入图片描述

四、yarn v1迁移pnpm

熟悉npm或者yarn的小伙伴,使用pnpm简直不要太欢乐!下面简单给出几行迁移命令~

4.1. 三行命令,从NPM/YARN迁移到PNPM
4.1.1 安装PNPM为全局包
npm i -g pnpm
4.1.2 项目中移除yarn/npm依赖项安装库

yarn:rm -rf node_modules yarn.lock
npm:rm -rf node_modules package-lock.json

4.1.3 使用PNPM安装项目依赖项
pnpm i
4.2. 可能遇到的问题
4.2.1 pnpm非平铺的依赖结构,导致曾经的“幽灵依赖”无法使用

这里建议将找不到的包显示声明,尽管pnpm支持这种依赖提升:shamefully-hoist ->true ,从命名也可以看出pnpm作者将其定义为“羞耻的”提升,因为这又走回了npm/yarn的老路,没有充分利用 pnpm 依赖访问安全性的优势。

4.2.2 如何指定安装某个包的依赖?
pnpm i --filter doctor-uni
4.2.3 yarn 的 nohoist 对 pnpm 的安装有没有影响?

没有影响

4.2.4 yarn 的 resolutions 对 pnpm 有没有影响

pnpm 为了兼容 yarn,同样会读取 resolutions 字段
详见:https://pnpm.io/package_json#resolutions

4.2.5 修改源文件

需要注意很多开发者使用patch-package对依赖的源码进行修改,但在monorepo模式下,尽管pnpm已经支持了patch-package,但会将所有使用相同版本的子项目源文件都修改,pnpm自己支持的patch命令同理。

4.3. 性能对比

对比了yarn v1与pnpm在我司项目的安装性能。
在这里插入图片描述

五、扩展-创建硬链接/符号链接

这里作者的电脑是mac,win用户的朋友们可以查查相关指令哈~下面只是带大家一起感受一下~

5.1. 符号链接:ln -s “源文件路径” “期望创建软链接文件路径”
如我想把下面的文件在桌面创建软链接

在这里插入图片描述
该文件的路径为:/Users/liangshuai/Desktop/MyCode/daily-test,故创建软链接命令如下: ln -s “/Users/liangshuai/Desktop/MyCode/daily-test” “/Users/liangshuai/Desktop”
在这里插入图片描述可以看到桌面已经生成了该文件的软链接,删除该文件夹也不会影响源文件。

5.2. 硬链接:ln “源文件路径” “期望创建硬链接文件路径”

如想把test_01.js创建一个软链接文件link.js在这里插入图片描述
进入项目根目录执行命令:ln “test_01.js” “link.js”在这里插入图片描述
可以看到项目中增加了一个link.js文件,修改link.js,test_01.js也会进行同步更改,因为它们指向同一个磁盘空间。
在这里插入图片描述

5.3. 小结

inode详见上文:2.1.1文件的本质

5.3.1 硬链接:

在这里插入图片描述
l 应用于同一文件系统上的一个物理文件

l 每个目录引用相同的inode 号

l 创建时链接数递增

l 删除文件时,递减链接数,当链接数为0时,该文件已被删除

l 硬链接的建立是不能越驱动器或分区的

l 语法为:ln filename linkname

5.3.2 软链接:

在这里插入图片描述
软链接并不使用相同的inode号,同时也不增加或减少目标文件inode的引用计数
l 创建的链接只是对应的指向源文件的路径,所以可以对目录进行链接,硬链接中只能对文件进行链接。

l 软链接是指向的一个文件的路径,所以可以跨越分区进行。

l 语法为:ln –s filename linkname

5.3.3 占用磁盘情况:

硬链接不占用磁盘空间,软链接占用的空间只是存储路径所占用的极小空间

5.3.3.1. 硬链接:

通过上文的分析,硬链接创建使用同一个inode号,指向相同的磁盘数据区域,所以不会占用额外的磁盘空间,但是有的小伙伴可能会有下面的疑问:

在根目录有一个12b的test.txt文件(total为4表示占用4k的block-磁盘空间)

在这里插入图片描述

接下来创建硬链接: ln “test.txt” “hardlink.txt”

在这里插入图片描述

这里发现创建硬链接后,文件大小(占用block)居然翻倍了(4k -> 8k)?下面我们再看看两个文件的inode号:

在这里插入图片描述

inode号是一样的?

这证明它们指向的是同一个数据块(在文件系统中一个inode号对应一个数据块群),并没有重新占用其他的数据块,所以也并不是复制了相同的文件,同时当改变其中一个文件的数据后,查看与之硬链接的文件其数据也是随之同步的,这迹象也表明对应的是一个数据块,而正真的问题并不是硬链接的问提,是这个 ls –l这命令进行统计文件总大小的时候并不是从磁盘进行统计的,而是根据文件属性中的大小叠加得来的。而硬链接的文件属性中的大小就是就是inode号对应的数据块的大小,所以total中进行统计就把各个文件属性中的大小加起来作为总和。

5.3.3.2. 软链接:

创建一个软链接:ln -s test.txt softlink.txt在这里插入图片描述

可以看到软链接就没有上面的问题了,软链接的inode号并不一样,而且软链接创建的文件也要比源文件小,这个链接文件softlinkl的数据中仅存储了一个路径而已,所以这部分大小只是路径的大小。

六、参考资料

  1. https://blog.csdn.net/qq_36179366/article/details/115477548
  2. https://zhuanlan.zhihu.com/p/546400909
  3. https://blog.csdn.net/wangjjmiao/article/details/116210796
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值