PNPM-2022
PNPM在2022年火的一塌糊涂,那么它究竟干了啥,有什么魔力能让那么多的前端同学为之神魂颠倒呢?
官方的介绍如下:
也就是说,PNPM是一个快速的,节省磁盘空间的包管理工具。
所以说,实际上pnpm带来的是时间和空间上的体验。下文将根据空间和时间俩个维度来说明pnpm干了些啥事。
空间
大家都知道,node_modules堪比黑洞,你根本不知道里面会有什么,有多少依赖层级。
一个项目尚且如此,那么几十上百个项目而言,那对于前端开发的本地磁盘而言就是个灾难。
那么如何去解决这个问题呢,最常规的思路就是根据具体某个包以版本维度给提取出来。例如A项目和B项目,都是React框架,都是基于react@16.8.0
版本。
我们可以想个办法,把这个react@16.8.0
版本的包给抽离出来,然后A项目和B项目都使用这个包。
显而易见,我们首先会想到软连接(类似windows的快捷方式)
没错,pnpm使用到了软连接的方案。于此同时,pnpm还使用到了硬链接。
那么问题来了,什么是软连接和硬链接。
在介绍软、硬链接之前,还需要介绍一个概念:inode,这个inode和后续的软、硬链接直接相关。
inode
inode (index node) 是指在许多“类Unix文件系统”中的一种数据结构,用于描述文件系统对象(包括文件、目录、设备文件、socket、管道等)。每个inode保存了文件系统对象数据的属性和磁盘块位置[1]。文件系统对象属性包含了各种元数据(如:最后修改时间[2]) ,也包含用户组(owner )和权限数据[3]。
这些文字略显苍白。实际上要理解inode,那得从计算机的文件存储开始说起。
大家都知道,文件一般都是存储在硬盘上,硬盘上的最小存储单位叫做 扇区(Sector) ,每个扇区储存512字节(相当于0.5KB),
操作系统读取硬盘的时候,不会一个个扇区地读取,因为这样效率太低了。实际上是一次性连续读取多个扇区,即一次性读取一个块(block) 。这种由多个扇区组成的 "块" ,是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件数据都储存在块(block) 中,那么我们还需要找到一个地方储存文件的元信息。比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为 "索引节点" 。
每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
inode包含的内容
* 文件的字节数
* 文件拥有者的User ID
* 文件的Group ID
* 文件的读、写、执行权限
* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
* 链接数,即有多少文件名指向这个inode
* 文件数据block的位置
如上所示,stat 命令可以获取Inode信息。
inode的大小
inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
inode编号
每个inode都有一个号码,操作系统用inode号码来识别不同的文件。
如果想要单独获取Inode编号,可以使用下面的命令。
$ ls -i package.json
// 会得出下面的,48563594就是inode编号
48563594 package.json
软连接
这个概念我们应该是不陌生的,比如说windows系统里面的快捷方式,实际上就是一种类似软连接的使用场景。
用inode简单的描述就是,俩个文件的inode编号不一样,但是呢俩个文件读取的内容是一样的,比如文件A和B,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的软链接(soft link) 或者符号链接(symbolic link) 。
软连接意味着依赖关系的存在,比如上面的A依赖着B,如果删除了B,就无法再打开A,会报错"No such file or directory"。
删除也是软连接和下文将要提到的硬链接最大的差别。
ln -s命令可以创建软链接。
// ln -s source target
ln package.json soft.json
硬链接
一般情况下,文件名和inode号码是一一对应,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。
这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为硬链接(hard link)
为什么删除不会影响其他文件呢,实际上的删除只是把inode中的链接数减1而已。删除是软连接和硬链接最大的差异。
ln命令可以创建硬链接
如下: package.json是项目中已经存在的文件,而test.json(作为target)是目标文件,命令执行后会生成一个test.json文件。
// ln source target
ln package.json hard.json
执行完命令,我们再查看一下package.json的inode信息,就会发现链接数会变成2。
stat -x package.json
pnpm中的软硬链接
pnpm中的软硬链接的使用,借用一张图就可以说明。
上面的图可能对很多人来说不是很友好,那我们就以实际的项目为例:
我们创建了一个**pnpm-test
**项目,这个项目呢只安装了一个react依赖包,pnpm项目结构如下:
node_modules/react
这个文件夹实际是个软连接,如下所以,我们使用ls -al命令可以找到这个文件的实际指向:node_modules/.pnpm/react@18.2.0/node_modules/react
ver@vermont pnpm-test % ls -al node_modules/react
lrwxr-xr-x 1 ver staff 37 1 11 14:19 node_modules/react -> .pnpm/react@18.2.0/node_modules/react
那么问题来了,node_modules/.pnpm/react@18.2.0/node_modules/react
这个文件夹下面的文件又是怎样的呢?
我们以node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
文件为例:
ver@vermont pnpm-test % ls -i node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
61754177 node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
接着我们再使用stat 命令来查看inode信息:
俩个关键信息:
inode编号:61754177
links: 3 (意味着这个文件就是个硬链接)
ver@vermont pnpm-test % stat -x node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
File: "node_modules/.pnpm/react@18.2.0/node_modules/react/package.json"
Size: 999 FileType: Regular File
Mode: (0644/-rw-r--r--) Uid: ( 501/ ver) Gid: ( 20/ staff)
Device: 1,18 Inode: 61754177 Links: 3
Access: Wed Jan 11 14:19:04 2023
Modify: Wed Jan 11 14:19:03 2023
Change: Wed Jan 11 14:25:17 2023
接着呢,我们又创建了一个**pnpm-test2
项目,和上面的pnpm-test**项目一样,只是项目名称不一样。
我们同样看一下node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
文件:
inode编号:61754177
links: 3
俩个inode编号一样,意味着这俩个文件读取的内容是一致的。当然了,根据上文硬链接的介绍,如果把其中一个文件删除,并不会对另一个文件有影响。
ver@vermont pnpm-test2 % stat -x node_modules/.pnpm/react@18.2.0/node_modules/react/package.json
File: "node_modules/.pnpm/react@18.2.0/node_modules/react/package.json"
Size: 999 FileType: Regular File
Mode: (0644/-rw-r--r--) Uid: ( 501/ ver) Gid: ( 20/ staff)
Device: 1,18 Inode: 61754177 Links: 3
Access: Wed Jan 11 14:19:04 2023
Modify: Wed Jan 11 14:19:03 2023
Change: Wed Jan 11 14:25:17 2023
关于pnpm空间上的优势
软硬链接的应用,以及全局硬链接的文件对于一个开发,电脑上有几十个项目而言,节省的空间是非常巨大的。
而npm每个项目都会把依赖安装一遍,会有非常多的重复文件。
时间
依赖安装流程
不管是什么,依赖安装的大体都可以分为下面三个步骤。
- Resolving。解析依赖树,也就是为了后续确定要fetch哪些安装包。
- Fetching。获取依赖的tar包。
- Writing。然后解压包,根据文件构建出真正的依赖树(即生成node_modules文件夹中的所有文件),这个阶段需要大量文件IO操作。
npm & yarn的依赖安装流程
总结起来,除了前面的解析和网络安装之外,后续解压然后把文件整到node_modules里面并且是以树的层级这一块的IO操作是相当耗时。
所以说npm和yarn 耗时的点就是:生成node_modules文件夹,而每个包在writing的时候,其他包是出于pending状态。所以时间就是所有包的writing时间的总和。
pnpm的依赖安装流程
pnpm的目录结构决定了它的项目的所有依赖包可以同时进行安装。关于pnpm的目录结构,可以在下文中进行详细的说明。
所以pnpm的差异化的耗时 大体就是 其中最耗时的一个包的writing时间
pnpm vs npm
npm的扁平化模式
npm从v3开始就实行了扁平化模式,还是以安装react来说:
看npm的项目结构: node_modules里面有3个包,分别是react、loose-envify、js-tokens。
有同学可能会问,我明明只安装了一个react,为什么node_modules里面还有loose-envify、js-tokens呢。这个其实就是npm的扁平化策略。
首先安装react,自然node_modules下面就是包含react这个包。
接着呢会解析react/package.json中的dependencies这个字段,发现有一个依赖包:loose-envify,于是npm把loose-envify也安装到了node_modules里面。
接下来也是一样的,解析loose-envify/package.json中的dependencies这个字段。
所以最终呢就是下面这个图里面的模式。
npm对于同一个包多个版本的安装规则:
首先还是一样会根据各个包dependencies逐级解析。
然后最后安装的话就是得看比如你的项目安装了react@18.0.0版本,然后又安装了一个第三方包,这个包的dependencies里面有一个react@16.0.0版本。
├── node_modules // 项目node_modules
├── react // 18.0.0版本
└── somePackage // 某个第三方包
├── node_modules
└── react // 16.0.0版本
pnpm的目录结构
如图所示:同样的安装react而已,pnpm的node_modules下面只有react一个包,当然还有一个.pnpm文件夹。
而node_modules/react这个文件其实是个软连接。它的实际指向上文也说过,就是node_modules/.pnpm/react@18.2.0/node_modules/react
所以说,pnpm项目中的node_modules中的包(非.pnpm文件夹中的) 全都是软连接,实际而.pnpm中的包则是硬链接,当然实际文件存储的磁盘位置则是在全局的storeDir里面(可以通过pnpm store path命令获取)。
pnpm的优势和不足
优势:
上文其实已经说明,核心就是空间和时间上的差异。当然还有一些其他的,例如关于peerDependencies的warning会更为友好
如下图,左侧为npm 安装antd时候关于peerDependencies的warning,右侧则为pnpm。
不足:
1、pnpm node_modules中的包实际是软连接,在某些不支持软连接的环境中pnpm就无法使用,比如说Electron。
2、迁移成本
虽然可以使用pnpm import命令基于lock文件(npm和yarn的lock文件都支持)生成pnpm的lock文件(pnpm-lock.yaml),然后进行依赖安装,可以满足大多数场景,但是优于pnpm的目录结构问题,可能会导致部分情况的path异常问题,比如我们的ecarx-build 会涉及部分path的定义(/node_modules/.ecarx-build目录),包括preinstall等钩子的应用都可能会涉及到path。
从npm迁移到pnpm
Pnpm官方已经给了支持,可以通过项目的lock文件(npm或者yarn)进行解析转换。
说不多说,直接开整:
1、rm -rf node_modules
2、pnpm import
3、pnpm i
4、pnpm run dev
这一步可能会出现一些依赖缺失的问题,从原来的lock文件中搜索到原有的版本号,然后按照这个版本号进行安装。
安装完之后继续执行pnpm run dev
命令,直到所有的依赖缺失问题解决,项目正常运行即可。
转载自:https://juejin.cn/post/7200191765516353593