在前端开发中,我们每天都会与各种npm包打交道,选择一个合适的包管理工具能显著提升开发体验和构建效率。近年来,pnpm作为一款新兴的包管理工具逐渐受到越来越多开发者青睐,它究竟有何魅力能让众多开源项目纷纷转投其怀抱?本文将带你深入了解pnpm的原理与优势。
一、为什么需要更好的包管理工具
1.1 npm与yarn的发展历程
在早期npm 1、npm 2时代,项目的node_modules会呈现出嵌套结构:
node_modules |
这种嵌套依赖树设计存在三个严重问题:
问题一:依赖层级太深
这会导致文件的路径过长的问题,尤其在Windows系统下文件路径默认最多支持256个字符。
问题二:包重复安装
比如foo和zoo都依赖于bar,那么bar就会在两者的node_modules中被安装两次,导致项目体积暴涨。
问题三:模块实例不能共享
比如React有一些内部变量,在两个不同包引入的React不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的bug。
后来yarn横空出世解决了这些问题,npm也在3.0版本中沿用了yarn的解决方案,这个解决方案就是扁平化依赖。
1.2 扁平化依赖带来的新问题
所谓的扁平化依赖就是将所有依赖铺平,放到同一级目录下:
node_modules |
虽然之前的问题得到了解决,但这种扁平化的处理方式自身也存在许多问题:
问题一:幽灵依赖
所谓的幽灵依赖是指我们明明没有在package.json的dependencies里声明某个依赖,但在代码里却可以import进来。因为项目依赖被铺平了,那么依赖的依赖自然也是可以被引入到项目中。
幽灵依赖带来的弊端很明显:我们显式依赖了A,A又依赖了B,这时候我们在项目中直接使用B是可以的,但如果某一天A不再依赖于B,那么我们项目中使用B的地方就会报错。
问题二:依赖结构不确定性
举例来说,如果项目同时依赖foo和bar,而它们都依赖不同版本的lodash,那么最终安装哪个版本是不确定的,完全取决于package.json中的声明顺序。
这就是为什么会产生依赖结构的不确定问题,也是lock文件诞生的原因,无论是package-lock.json还是yarn.lock,都是为了保证install之后都产生确定的node_modules结构。
问题三:非法访问依赖
由于扁平化结构,如果A依赖B,B依赖C,那么A当中是可以直接使用C的,但A并没有声明C这个依赖,这就是非法访问。
在monorepo项目中,如果A依赖X,B依赖X,还有一个C,它不依赖X,但它代码里面用到了X。由于依赖提升的存在,npm/yarn会把X放到根目录的node_modules中,这样C在本地是能够跑起来的。但试想一下,一旦C单独发包出去,用户单独安装C,那么就找不到X了,执行到引用X的代码时就直接报错了。
正是这些问题促使了pnpm的诞生。
二、pnpm是什么
pnpm(Performant NPM)是一个快速的、节省磁盘空间的包管理工具,同时它还对monorepo有良好的支持。
# 安装pnpm |
它的用处与npm和yarn并没有什么本质区别,甚至连用法都十分相似,但却在性能和安全性方面有显著优势。
三、pnpm核心优势
3.1 安装速度快
根据官方文档的benchmark测试数据,pnpm在大多数场景下的安装速度都要优于npm和yarn。
可以看到,pnpm在绝多大数场景下,包安装的速度都是明显优于npm/yarn,速度会比npm/yarn快2-3倍。
对yarn比较熟悉的同学可能会说,yarn不是有PnP安装模式吗?直接去掉node_modules,将依赖包内容写在磁盘,节省了node文件I/O的开销,这样也能提升安装速度。
但总体而言,pnpm的包安装速度还是明显优于yarn npm。
这是因为pnpm采用了并行的网络请求和本地缓存策略,减少了等待时间。
3.2 节省磁盘空间
pnpm内部使用**基于内容寻址存储(CAS - Content-addressable store)**的方式来存储依赖。
硬链接与软链接机制
- 硬链接(hard link):多个文件平等地共享同一个文件存储单元
- 符号链接/软链接(Symbolic link):一种特殊的文件,包含有一条指向其它文件或者目录的引用
软链接其实很好理解,它就相当于Windows系统中的快捷方式。一个符号链接文件仅包含有一个文本字符串,其被操作系统解释为一条指向另一个文件或者目录的路径。它是一个独立文件,其存在并不依赖于目标文件。
硬链接可以理解成给同一个文件创建了多个入口,它们都指向磁盘上的同一个存储位置。删除任何一个硬链接不影响其他链接,只有当所有硬链接都被删除时,文件才会被真正删除。
pnpm的磁盘空间优化策略包括:
不会重复安装同一个包:用
npm/yarn的时候,如果100个项目都依赖lodash,那么lodash很可能就被安装了100次,磁盘中就有100个地方写入了这部分代码。但使用pnpm则只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用硬链接。不同版本复用:即使一个包的不同版本,
pnpm也会极大程度地复用之前版本的代码。比如lodash有100个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入101个文件,而是保留原来100个文件的硬链接,仅仅写入那一个新增的文件。
3.3 安全性高
pnpm自创了一套依赖管理方式——虚拟存储目录,很好地解决了幽灵依赖和非法访问的问题。
在pnpm中,只有在package.json中显式声明的依赖才能被访问,未声明的依赖是无法引入的,这保证了项目的安全性。
3.4 支持Monorepo
pnpm对monorepo的支持体现在各个子命令的功能上:
# 在根目录下执行,所有package都会被添加依赖 |
四、pnpm依赖管理原理
4.1 npm/yarn的安装原理
当执行npm/yarn install命令之后,首先会构建依赖树,然后针对每个节点下的包会经历以下四个步骤:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应版本依赖的tar包到本地离线镜像
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的node_modules目录
4.2 node_modules目录结构
使用pnpm安装express后,目录结构如下:
node_modules |
可以明显感觉到pnpm的目录结构更加合理:项目只依赖了express,那么node_modules中就只存在express的文件。
展开.pnpm文件夹,我们可以找到其真正的位置:express的真正位置位于.pnpm/express@4.17.1/node_modules/express。
这也代表了pnpm中的依赖规律,也是<package-name>@version/node_modules/<package-name>这种目录结构。
4.3 软链接与硬链接的协作
pnpm的依赖规律是<package-name>@version/node_modules/<package-name>这种目录结构:
- 所有的依赖都是从全局
store硬链接到了node_modules/.pnpm下 - 项目中的直接依赖通过软链接指向
.pnpm中的实际位置 - 依赖包中的每个文件再硬链接到
.pnpm store
.pnpm |
这种设计将包本身和依赖放在同一个node_module下面,与原生Node完全兼容,又能将package与相关的依赖很好地组织到一起,设计十分精妙。
五、pnpm工作空间(Workspace)完全指南
5.1 什么是Monorepo
Monorepo(单体仓库)是一种将多个项目存放在同一个代码仓库中的开发策略。相比传统的多仓库方案,Monorepo可以让代码共享更加方便版本管理更加统一。
pnpm从设计上就原生支持Monorepo,通过workspace功能可以轻松管理多个包。
5.2 创建Workspace项目
首先在项目根目录创建pnpm-workspace.yaml文件:
# pnpm-workspace.yaml |
然后创建项目结构:
my-monorepo/ |
5.3 Workspace常用命令
安装所有依赖
# 安装workspace下所有packages的依赖 |
在所有package中安装依赖
# 在根目录执行,所有package都会被添加lodash依赖 |
使用filter过滤
# 只在pkg1中安装 |
在特定package中执行命令
# 在pkg1中执行构建命令 |
5.4 Workspace内部依赖
在workspace中,不同package之间可以相互引用:
# 将pkg2添加到pkg1的依赖中 |
这会自动在packages/pkg1/package.json中添加:
{ |
使用workspace:*可以引用当前workspace中的版本,确保所有package使用一致的内部依赖版本。
5.5 发布配置
在根目录的package.json中配置发布相关设置:
{ |
六、pnpm基本使用
pnpm的使用成本非常低,因为它的基础命令和npm/yarn基本相似。
6.1 安装依赖
# 安装项目所有依赖 |
6.2 更新与卸载
# 更新依赖到最新版本 |
6.3 运行脚本
// package.json |
# 运行脚本 |
6.4 其他常用命令
# 查看依赖信息 |
七、三者功能对比一览
| 功能 | pnpm | Yarn | npm |
|---|---|---|---|
| 工作空间支持 | ✅ | ✅ | ✅ |
| 隔离的node_modules | ✅ | ✅ | ✅ |
| 提升的node_modules | ✅ | ✅ | ✅ |
| Plug’n’Play | ✅ | ✅ | ❌ |
| 自动安装对等依赖 | ✅ | ❌ | ✅ |
| 零安装 | ❌ | ✅ | ❌ |
| 修补依赖项 | ✅ | ✅ | ❌ |
| 管理Node.js版本 | ✅ | ❌ | ❌ |
| 内容可寻址存储 | ✅ | ✅ | ❌ |
| 动态包执行 | ✅ | ✅ | ✅ |
| 副作用缓存 | ✅ | ❌ | ❌ |
| 目录(Catalogs) | ✅ | ❌ | ❌ |
| 配置依赖项 | ✅ | ❌ | ❌ |
| 脚本运行前自动安装 | ✅ | ❌ | ❌ |
| 列出许可证 | ✅ | ✅ | ❌ |
核心差异解读
pnpm vs npm/yarn 最大的区别在于node_modules的结构:
- npm/yarn采用扁平化结构,会产生幽灵依赖
- pnpm采用虚拟存储目录,依赖严格隔离
内容可寻址存储让pnpm能够:
- 同一包的不同项目只安装一次
- 不同版本之间复用相同文件
pnpm独有的功能:
pnpm env:管理Node.js版本pnpm patch:修补依赖项Catalogs:目录功能
八、总结
pnpm相比npm/yarn的核心优势在于:
- 更快:安装速度比
npm/yarn快2-3倍 - 更省空间:通过硬链接机制避免重复安装,节省大量磁盘空间
- 更安全:严格的依赖管理避免幽灵依赖和非法访问
- 更规范:清晰的目录结构,原生支持Monorepo
综合来看,pnpm是一个相比npm/yarn更为优秀的包管理方案。推荐在新项目中使用,体验现代包管理工具带来的效率提升。
参考资料: