pnpm为什么这么快(安装)且省(空间)?

为什么是pnpm

npm v3版本之前,依赖采用嵌套的方式,但是由于window系统对文件路径长度是有限制的,不能超过256字符,从而无法操作深层级文件.

v3版本开始采用平铺的方式,但是又会出现幽灵依赖(子依赖提升造成的,即可以访问未声明的 npm 包).

而且两者都有磁盘占用的问题:如果10个项目中都使用了模块A,那么A会被安装10次,造成了磁盘空间的浪费.

而pnpm将所有依赖包存储在.pnpm-store , 每个相同的依赖包只会被安装一次, 从而安装速度更快. 新增 .pnpm 文件夹, 并且使用链接的方式来组织 node_modules 结构, 使依赖项更加直观, 而且没有了幽灵依赖.

.pnpm-store 下的目录:

.pnpm store 里的这些文件代表什么意思呢?

实际上 pnpm 不是直接把包文件放到pnpm store的, 而是经过了处理, 利用 CAS 的原理,下面会讲到什么是 CAS.

工作原理

CAS内容寻址存储(Content-Addressed Storage)

简称CAS,是一种存储信息的方式,根据内容而不是位置进行检索信息的存储, 也就是说文件的名称是跟内容相关的。 比如通过内容的hash算法生成。

利用 CAS , pnpm做到了:

1、不管有多少项目依赖一个包, 这个包都只会下载存储一次

2、平铺结构中也可以区分不同版本的依赖包

3、保证包的完整和正确

我们常用的 git 实际上也使用了 CAS 这种寻址方式.

pnpm包的寻址过程

第一层:在项目的 node_modules 目录下寻找依赖项,只有在 package.json 中定义的依赖才会在这层目录下找到。依赖项的子依赖也在这个包内寻找,从而做到了没有歧义,也规避了幽灵依赖问题.

第二层: 现在就是类似npm2.X 的结构了,然后为了解决包复用的问题, pnpm 使用了软链接: node_modules/dayjsnode_modules/.pnpm/dayjs@1.11.10/node_modules/dayjs ,使用 .pnpm 文件夹这种方式来代替原来 npm3.X的直接将包在 node_modules 下打平的设计.

在vscode中可以安装 Symbolic Link Jump Tools 插件来看到软链接的指向

第三层:node_modules/.pnpm/dayjs@1.11.10/node_modules/dayjs → 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx ,这时候指向的是全局的路径,依赖存储在全局,从而实现跨项目复用.

.pnpm-store存储

什么是inode和硬链接、软链接

inode 是用来储存文件的元信息的内存区域,元信息包括文件的创建者、文件的创建日期、文件的大小等等.

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息, 其中就有链接数,即有多少文件名指向这个inode.

硬链接: 一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是允许多个文件名指向同一个inode号码.

这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link).

文件硬链接不管有多少个,都指向的是同一个 inode 节点,这意味着当你修改源文件或者链接文件的时候,都会做同步的修改. 每新建一个硬链接会把节点连接数增加,只要节点的链接数非零,文件就一直存在。因此不管你是删除硬链接还是源文件,文件都一直生效.

Q: 那么什么时候文件会被删除呢?

A: 指向inode的链接个数为0的时候,即指向inode的链接都被删除的时候。

软链接: 文件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"链接数"不会因此发生变化.

shell 复制代码
# 创建硬链接
ln 源文件 目标文件
# 查看文件inode
ls -i 文件名
# 创建软链接:
ln -s 源文件 目标文件

新建一个 demo.js 的硬链接, 然后查看两者的 inode , 可以看到两者具有相同的 inode :

注: inode和链接部分内容摘自: 理解inode - 阮一峰的网络日志

几个问题:

  • 怎么获取 pnpm-store 的路径:
lua 复制代码
pnpm store path
  • pnpm-store 是怎么存储依赖的:

pnpm-store 并没有直接存储文件内容,而是通过CAS内容寻址的方式,采用了文件 hash 值前两位作为二级目录,余下hash值作为文件名来存储文件的。实际上是通过计算文件的 integrity ,再把 integrity进行 base64编码,再转换16进制,得到这一长串文件名的。

  • package-lock.json 中的 integrity 起什么作用?

用来验证资源的完整性,即是否是期望加载的资源,而不是被篡改了的内容(Subresource Integrity)。

清理pnpm存储

pnpm store prune

从存储中删除_未引用的包,未引用的包是系统上的任何项目中都未使用的包。

pnpm在monorepo的使用

什么时候适合使用 monorepo ?

monorepo 的缺点是不能对子目录进行权限管理, 那么在不需要管理子目录权限的时候就可以使用.比如工具库、组件库的开发. 否则就不太适合了, 那不用monorepo 怎么解决多个项目间的代码复用问题呢, 我们可以考虑 npm包、模块联邦、git submodule等方案.

pnpmmonorepo使用起来很方便,只需新建一个 pnpm-workspace.yaml 文件, 在文件中声明工作区即可:

makefile 复制代码
packages:
  - "sub-a"
  - "sub-b/dict"

运行多脚本

运行多个脚本以前我们会使用 npm-run-all 这样的包,现在可以直接运行 :

arduino 复制代码
pnpm run "/^watch:.*/"

运行所有以 watch: 开头的脚本:

直接运行 .bin

只要你安装了包,你就可以在脚本中使用它,就像常规命令一样。例如,如果你已经 eslint 安装,你可以编写一个脚本,如下所示:

lua 复制代码
lint": "eslint src --fix

pnpm option

-r 递归,这将从每个包的"scripts"对象运行任意命令

---filter 按名称或者关系选取包总而执行命令, 可以使用这个命令执行子目录

xml 复制代码
pnpm --filter <package_selector> <command>

使用过程中的问题

pnpm 功能可以说是很强大了, 而且使用的体验很好, 但使用的过程中发现了一个模块被提升的问题:

package.json 中没有声明的依赖 eslint-scope 却出现在node_modules中

这是因为pnpm的public-hoist-pattern 默认值为['eslint ', 'prettier '],会将与模式匹配的依赖项提升到根模块目录,在 .npmrc 文件进行设置就可解决:

ini 复制代码
 public-hoist-pattern=[]
相关推荐
编程零零七2 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦4 小时前
JavaScript substring() 方法
前端
无心使然云中漫步5 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者5 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_5 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋6 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120536 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢6 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写8 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.8 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html