探究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,是一种存储信息的方式,根据内容而不是位置进行检索信息的存储。

传统地址寻址,文件名跟文件内容没有必然关系。如果文件位置变化,原来的访问地址会失效。

而CAS,因为内容寻址的key是通过内容的hash算法生成,hash key对应的值是具体的文件内容,一旦文件发生任何改变,内容地址也会发生改变。

利用 CAS , pnpm做到了:

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

2、版本锁定, CAS结合哈希地址可以确保依赖版本的一致性

我们常用的 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 ,这时候指向的是全局的路径,依赖存储在全局,从而实现跨项目复用.

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

储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等. 这种储存文件元信息的区域就叫做inode.

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

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

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

文件硬链接不管有多少个,都指向的是同一个 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=[]
相关推荐
沿着路走到底15 分钟前
JS事件循环
java·前端·javascript
子春一233 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶39 分钟前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied2 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
羽沢313 小时前
ECharts 学习
前端·学习·echarts