PNPM-2022

lxf2023-03-17 16:21:01

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的位置

PNPM-2022

如上所示,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-2022

pnpm中的软硬链接

pnpm中的软硬链接的使用,借用一张图就可以说明。

PNPM-2022

上面的图可能对很多人来说不是很友好,那我们就以实际的项目为例:

我们创建了一个**pnpm-test**项目,这个项目呢只安装了一个react依赖包,pnpm项目结构如下:

PNPM-2022

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每个项目都会把依赖安装一遍,会有非常多的重复文件。

时间

PNPM-2022

依赖安装流程

不管是什么,依赖安装的大体都可以分为下面三个步骤。

  • 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这个字段。

所以最终呢就是下面这个图里面的模式。

PNPM-2022

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的目录结构

PNPM-2022

如图所示:同样的安装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。

PNPM-2022 PNPM-2022 不足:

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命令,直到所有的依赖缺失问题解决,项目正常运行即可。