为什么是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/dayjs
→ node_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
等方案.
pnpm
的 monorepo
使用起来很方便,只需新建一个 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=[]