探究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=[]
相关推荐
明辉光焱17 分钟前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛38 分钟前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
开心工作室_kaic1 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
晨曦_子画1 小时前
用于在 .NET 中构建 Web API 的 FastEndpoints 入门
前端·.net
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:在 PDF 文件中添加图像作为页面背景
前端·pdf·.net·spire.pdf
咔咔库奇1 小时前
ES6基础
前端·javascript·es6
Jiaberrr1 小时前
开启鸿蒙开发之旅:交互——点击事件
前端·华为·交互·harmonyos·鸿蒙
徐小夕2 小时前
Flowmix/Docx 多模态文档编辑器:V1.3.5版本,全面升级
前端·javascript·架构
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库