新一代包管理工具 pnpm(npm包管理工具使用场景)
liuian 2025-05-03 15:15 77 浏览
背景
如果你从事过前端方面的工作和开发,相信你对 npm 和 yarn 这样的工具已经再熟悉不过了。作为包管理工具,npm 已经有了长足的历史[1]。一个项目初始化过程都需要通过 npm install 命令安装相关的依赖到 node_modules 目录下,对于较大型的前端项目,node_modules 的大小很多时候是超乎我们想象的。而 npm 在版本更新迭代的过程中,也一直在优化这个问题,下面我们就简要聊聊 npm 的发展历程。
npm 发展
npm2
在 npm2 发展阶段,安装依赖是相对比较直接的,它会直接按照配置文件 package.json 中的依赖项去下载相关依赖包,而依赖包的组织形式则是按照树形结构去排列的。由于不同的包的依赖关系在版本上差异较大,依赖关系相对复杂,所以 npm2 直接按照配置去下载并组织依赖的方式,是简单明晰的做法,保证了各个依赖的独立性,在依赖变更时,相互并不影响,其关系可以通过下图来描述:
从上图中,我们可以看到
- A、B、C 包相互独立
- A、B、C 包可能会依赖相同的包,比如 D@1.0
- A、B、C 包可能会存在较深的依赖层级,比如 C package
其中 2 和 3 这两点的负面影响会随着项目复杂度而上升,可能会导致的几个问题
- 较大的冗余。多次下载的相同的依赖包 D@1.0,无法实现共享
- 较深层级的依赖树。
- Too many dependencies break the Windows file system[2]
- Maximum Path Length Limitation[3]
- Why does the 260 character path length limit exist in Windows?[4]
- node_modules 依赖包路径过长,超出操作系统最长路径限制( windows:260 字符,macos:1024 字符),参见:
- 太深的层级导致文件查找复杂度上升,严重影响性能,增加耗时
Note: 通过 npm ls --depth=n 查看项目相关依赖层级深度
npm3
为解决 npm2 中存在的冗余和依赖树问题,npm3 对依赖项进行了依赖扁平化讨论和处理[5]
扁平化具体来讲就是依赖不在按照树型进行安装,而是安装将依赖安装在同级目录下,npm install安装依赖时,会按照配置文件 package.json 里的依赖顺序进行解析,遇到新包就把它放在第一层级的目录(如 D@1.0、E@1.0、F@1.0),后面如果遇到第一级目录已有的包,会先进行依赖版本判断,如果版本一样则忽略,否则会按照 npm2 的方式依次挂在依赖包目录下,这样处理的原理遵循了`Nodejs`的依赖解析规则[6]:当前目录下没有找到node_modules,它将递归解析父目录下的node_modules。
使用 npm3 安装依赖后如下图:
这种扁平化处理方式一定程度上缓解了冗余和依赖树问题,同时 npm3 还支持动态安装更新包,如果依赖有更新,可以通过 npm dedupe 命令对依赖树进行优化。
但是 npm3 也存在部分问题,比如:
- phantom_deps(幻影依赖)[7]。npm3不会以确定的方式安装依赖项。举例来说:我们在 NodeJS 中 require() 的函数,不需要考虑配置文件 package.json 中是否有该依赖项。这可能会导致依赖版本不兼容,并且开发者不容易发现;另外,由于`Nodejs`的依赖解析规则[8],这还会导致幻影 node_modules ,即依赖向上查找,可能会越过代码目录自身的 node_modules 。如下:
- my-monorepo/
- package.json
- node_modules/
- semver/
- ...
- my-monorepo/my-library/
- package.json
- lib/
- index.js
- node_modules/
- brace-expansion
- minimatch
- ...
my-monorepo/my-library/lib/index.js 可能使用的是my-monorepo/node_modules 中的依赖,而非自身目录 my-monorepo/my-library/node_modules
- npm doppelgangers(npm 分身)[9]。简单来讲,npm 分身是指同一个依赖的不同版本会出现在 node_modules 中,比如项目中同时依赖了 A@1.0.0 和 A@2.0.0,无论是扁平化处理A@1.0.0 或 A@2.0.0,另一个依赖还是会被重复,如果这样的分身较多,就会导致一些潜在问题,比如扩展包大小变大、相关类型校验交叉等
npm5
npm5 通过添加 lock 文件来记录依赖树信息,进行依赖锁定,从而唯一确定 node_modules 的结构,这样处理可以保证团队成员使用同一份node_modules依赖结构。但是,我们前文提到的平铺式的算法的复杂性、幻影依赖和分身问题仍然没有解决。
pnpm 简介
前文我们大致梳理了 npm 的发展和遗留问题。而 pnpm 比较巧妙地解决了它们,并且极大地提升了依赖包管理的效率。
pnpm 指 performant npm(高性能的 npm),如 pnpm 官网[10]所言,它是快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepos。
举例来说,如果项目中,你使用了某个依赖项的多个版本,那么 pnpm 只会将有差异的文件添加到仓库。如果某个依赖包有 100 个文件,而它的新版本只改变了其中 1 个文件。那么 pnpm update 时只会添加 1 个新文件,而不会复制整个新版本的所有包。此外。所有文件都会存储在硬盘上的某一位置。当依赖包被被安装时,其中的文件会硬链接到这一位置,而不会占用额外的磁盘空间。同时,项目中允许共享同一版本的依赖。接下来我们先了解下 pnpm 的使用效果
pnpm 效果
与 npm、yarn、yarn pnp 工具链效果对比,来自 pnpm benchmarks[11]
action | cache | lockfile | node_modules | npm | pnpm | Yarn | Yarn PnP |
install | 1m 9.5s | 15.3s | 16.6s | 23.6s | |||
install | 2.4s | 1.3s | 2.3s | n/a | |||
install | 14.8s | 4s | 6.8s | 1.5s | |||
install | 21.8s | 8.9s | 11.2s | 6.2s | |||
install | 35.4s | 13.4s | 12s | 17.9s | |||
install | 3.1s | 1.9s | 7s | n/a | |||
install | 2.4s | 1.3s | 7.6s | n/a | |||
install | 3s | 6.1s | 11.8s | n/a | |||
update | n/a | n/a | n/a | 2.3s | 11.8s | 15.5s | 28.3s |
从上表数据我们可以看出,pnpm 的各项性能均比其它包管理工具有优势,那你可能会想,为什么 pnpm 有如此优越的表现,接下来我们聊聊 pnpm 的主要原理
pnpm 的原理
pnpm 主要有两个不同与其包管理工具的特性:
基于硬链接的 node_modules
pnpm 创建从全局存储到项目中 node_modules 文件夹的硬链接[12],而硬链接指向磁盘上原始文件所在的同一位置,具体来说就是 node_modules 中每个包的每个文件都是来自内容可寻址存储[13]的硬链接,简言之,就是特定版本和名称的包全局只有一份。举例来看:
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
node_modules 下面的唯一文件夹叫做 .pnpm, .pnpm 下面是一个 <PACKAGE_NAME@VERSION>文件夹,而在其下面 <PACKAGE_NAME> 的文件夹是一个基于内容可寻址存储的硬链接。同时,我们也可以通过 pnpm root 命令来打印当前项目中存放模块(modules)的有效目录
基于依赖解析的软链接 symlinks
观察以下依赖包结构
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
我们可以看到在 foo@1.0.0/node_modules/bar 内引用了 bar 的软链接 ../../bar@1.0.0/node_modules/bar,而在项目里引用 foo 的软链接 ./.pnpm/foo@1.0.0/node_modules/foo,如果项目内新增一个依赖包 qar@2.0.0,则其引用结构如下:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
根据前文我们介绍到的`Nodejs`的依赖解析规则[14],foo@1.0.0/node_modules/foo/index.js 中所需的依赖包 bar,实际上使用的是bar@1.0.0/node_modules/bar中的内容,因此,只有真正在依赖项中的包才能被访问到。而对于不同的 peer dependencies 的依赖解析原理,可以参考这里 How peers are resolved[15]
通过基于硬链接的node_modules和基于依赖解析的软链接原理,我们了解到,当我们在相同操作系统下第二次安装同一个依赖包时,我们需要做的仅仅是创建一个该依赖包对应的硬链接,对于同一个依赖包的不同版本,也只有不同的部分会被重新保存起来,而具体有没有 pnpm 是在哪里判断的呢?全局的 pnpm 索引文件在 ~/.pnpm-store/v3/files。基于此,使用硬链接让依赖包的安装速度非常快,同时也去除了冗余,节省较大磁盘空间。
symlinks 符号连接[16]
pnpm 使用
pnpm 的具体使用这里我们不展开介绍了,可以查看官网使用方法[17]和CLI 命令[18]即可。这里只提几个有意思的点
CI 集成
在 GitHub Actions 上,你可以像这样使用 pnpm 安装和缓存依赖项,配置文件目录: .github/workflows/NAME.yml
name: pnpm Example Workflow
on:
push:
jobs:
build:
runs-on: ubuntu-20.04
strategy:
matrix:
node-version: [15]
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.20.3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
pnpm 除了在开发体验方面的优越表现,在项目集成方面也毫不逊色,对于较大型项目从 npm 或 yarn到pnpm迁移过程后,也得到了极大的优化,结果如下:
Without cache | With cache | |
yarn 2 (without dedupe) | 6min 31s | 1min 11s |
yarn 3 (without dedupe) | 4min 50s | 57s |
yarn 3 | 4min 1s | 50s |
yarn 3 (optimized) | 1min 10 | 45s |
pnpm | 58s | 24s |
通过以上数据,我们可以 pnpm 在 CI 应用中的良好表现。
具体可以参考这篇最佳实践 A story of how we migrated to pnpm[19]
pnpm 前置
项目中使用 pnpm 时,如果你不希望项目内其他人使用 npm i 或 yarn这类包管理器,可以在 package.json 配置文件中添加预安装 preinstall 配置项,从而规范使用统一的包管理器。
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
管理 NodeJS 版本
在以前,如果你同时支撑了多个项目,而且需要在其中切换,你可能需要切换不同的 NodeJS 版本,也许你会用到像 nvm 或 Volta[20] 这样的 NodeJS 版本管理器,而 pnpm 从 v6.12.0 版本后支持了 pnpm env[21] 命令,你可以使用它来安装并指定使用哪个版本的 NodeJS ,是不是方便了很多。
monorepo 支持
因为pnpm 对 monorepos 的大力支持,像 Vue、Vite 这些开源项目也转而使用了它。使用pnpm run 结合 --filter 、 --recursive 和 --parallel 选项,可以指定特定包,并高速执行相关命令。这样做的好处是之前要另外安装 lerna 这种 monorepo 管理工具的场景,现在 pnpm 可以包揽了。详细文章可以参考这里 pnpm vs Lerna: filtering in a multi-package repository[22]
总结
本文从 pnpm 的出现背景开始,简要介绍了 npm 的发展过程及存在的问题,继而对 pnpm 及其效果进行了简介,重点讲述了 pnpm 的实现原理,并从应用侧选择了四个点展开。
pnpm 作为新一代包管理器,自身有不少优越的表现,它通过硬链接和软链接的方式,解决了 npm幻影依赖和分身问题,并且较好地解决了依赖包复用问题,从而实现了依赖包高效快速的安装。需要特别注意的是 pnpm 严格遵循了 Nodejs 依赖解析规则,规避了之前任意依赖包的访问修改问题。
当然,pnpm 使用过程中也存在一些问题,包括 Vue 官方在迁移过程中,也处理过部分问题。另外,一些包也存在兼容性问题,由于包自己实现了模块解析,并没有遵循相关规范。但 pnpm 也提供了相关解决方法。具体参考 pnpm FAQ[23]
综上,pnpm 是一个功能全面,性能优越的包管理器,快来体验 pnpm 吧。更多文章,欢迎扫码关注微信公众号 【青梅主码】
参考资料
[1]
历史: https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-1.md
[2]
Too many dependencies break the Windows file system: https://github.com/npm/npm/issues/3697
[3]
Maximum Path Length Limitation: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd
[4]
Why does the 260 character path length limit exist in Windows?: https://stackoverflow.com/questions/1880321/why-does-the-260-character-path-length-limit-exist-in-windows
[5]
依赖扁平化讨论和处理: https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-3.md
[6]
Nodejs的依赖解析规则: https://nodejs.org/api/modules.html#all-together
[7]
phantom_deps(幻影依赖): https://rushjs.io/pages/advanced/phantom_deps/
[8]
npm doppelgangers(npm 分身): https://rushjs.io/pages/advanced/npm_doppelgangers/
[9]
pnpm 官网: https://pnpm.io/
[10]
pnpm benchmarks: https://pnpm.io/zh/benchmarks
[11]
硬链接: https://zh.wikipedia.org/wiki/%E7%A1%AC%E9%93%BE%E6%8E%A5
[12]
内容可寻址存储: https://en.wikipedia.org/wiki/Content-addressable_storage
[13]
How peers are resolved: https://pnpm.io/zh/how-peers-are-resolved
[14]
symlinks 符号连接: https://zh.wikipedia.org/wiki/%E7%AC%A6%E5%8F%B7%E9%93%BE%E6%8E%A5
[15]
使用方法: https://pnpm.io/zh/pnpm-cli
[16]
CLI 命令: https://pnpm.io/zh/cli/add
[17]
A story of how we migrated to pnpm: https://divriots.com/blog/switching-to-pnpm
[18]
Volta: https://volta.sh/
[19]
pnpm env: https://pnpm.io/zh/cli/env
[20]
pnpm vs Lerna: filtering in a multi-package repository: https://medium.com/pnpm/pnpm-vs-lerna-filtering-in-a-multi-package-repository-1f68bc644d6a
[23]
pnpm FAQ: https://pnpm.io/faq#pnpm-does-not-work-with-your-project-here
- 上一篇:微信小程序入门1(微信小程序入门书籍推荐)
- 下一篇:常用工具列表(常用工具软件)
相关推荐
- 电脑品牌型号在哪里看(电脑选什么牌子的好)
-
查看自己电脑品牌型号和具体配置的具体方法:1.右键点击桌面上的“这台电脑”图标,弹出的菜单中选择“属性”。2.在此界面即可看到你的电脑的CPU品牌(AMD)、型号(A6-5200)、频率(2.0GHz...
- 网卡驱动装不上怎么回事(网卡驱动装不了怎么办)
-
可能原因如下1、这种情况是因为系统中没有集成电脑网卡所对应的驱动程序。2、这种情况可以利用鲁大师查询网卡的具体型号,然后去官网或者网络上下载对应的驱动重新安装就可以正常上网了。可能原因如下:1,估计是...
- 手机输入法怎么打繁体字(手机怎样输入繁体)
-
步骤如下:1.在手机设置页面找到输入法设置,如果能搜索,会帮助找到输入法设置页面。2.在系统的输入法设置页面,找到输入法自身的设置页面入口。3.在输入法自设难度设置页面,找到简繁切换的设置页面。4.勾...
- photoshop 下载(photoshop下载需要花钱吗)
-
怎样下载photoshop的步骤方法如下面所示:1.首先第一步的步骤是在我们的电脑桌面上找到电脑管家并紧接着点击它(如果没有电脑管家需要提前安装)。2.然后一步的方法是点击屏幕右下方的工具箱。3.最后...
- 联想官方网站驱动下载官网(联想官方网站驱动下载官网安装)
-
拯救者官网下驱动的方法:打开联想服务官网(support.lenovo.com.cn),页面滚动下拉找到“帮助与支持”板块,点击“驱动和软件下载”。下载联想拯救者的驱动很简单。简单联想官网提供详细的驱...
- win7一键重装(win7一键重装系统纯净版不需要激活哪个好)
-
1.制作一个u盘pe2.下载win7系统镜像到u盘里面3.u盘插入电脑4.启动电脑按F12或DE5.选择usb进入pe6.打开一键安装7.选择系统8.等待安装,都是自动安装无需手动9.安装系统完成后就...
- 硬盘低格工具软件(硬盘低格软件推荐)
-
万能低格工具llftool好万能低格工具llftool是一款强大易用的硬盘低级格式化软件,支持硬盘、移动硬盘、内存卡、u盘等等存储设备的低格功能,过程快速方便,性能安全稳定。另外,...
-
- 笔记本电脑过热保护自动关机
-
笔记本电脑发热可能是因为散热不行导致cpu过热自动关机保护,可以这样处理:1)清洁笔记本侧面和底部的散热孔,保证可以充分散热;2)外接风扇进行辅助散热,如问题依然存在,按照步骤3处理;3)拆卸后盖,清洁风扇叶片上的灰尘,同时,更换导热片下方...
-
2026-01-12 01:55 liuian
- 英特尔cpu查询(英特尔cpu查询保修)
-
英特尔官网序列号的查询,1可以通过BIOS设置或者CPU-Z等软件查询英特尔序列号。2英特尔序列号是由英特尔公司推出的一种唯一标识符,用于区分不同的处理器芯片。3除了查询英特尔序列号,CPU-Z...
- 电脑可以还原系统吗(电脑可以还原到出厂设置吗)
-
方法/步骤分步阅读1/11Windows7系统打开或者关闭系统还原点的方法Windows7系统关闭系统还原右键桌面计算机图标-属性;2/11在打开的系统窗口,我们点击左侧的高级系统设置;3/11在...
- visio软件密钥(microsoft visio密钥)
-
在控制面板\所有控制面板项\程序和功能中找到,右键----更改
- 1920x1080和2k差别大么(4k为啥没1080p清晰)
-
区别:图像清晰度不同。2K的清晰度高于1920×1080。电视或显示屏领域上,1080P,一般对应1920*1080分辨率,2K则对应2560*1440分辨率。两者的主要区别在于像素点的多少,在尺寸相...
- 路由器恢复出厂设置会怎么样
-
现在的很多路由器都很智能化,我们在日常使用的时候,很多时候出现卡死或者忘记了路由器的登录密码等问题,这个时候我们就需要对路由器进行重置或者是恢复出厂设置,这样就可以用初始化的用户名和密码登录路由器了...
- 电脑密码忘了如何取消开机密码
-
1/5第一种方法:在开机时,按下F8进入带命令提示符的安全"模式输入"netuser+用户名+123456/add"可把自己的密码设置为"123456",这样进入控制面板就可以清除自己用户密码了。...
- 安卓手机exe文件怎么打开(安卓手机用什么打开exe文件)
-
在Android操作系统中,通常情况下是无法直接运行.exe文件的,因为Android手机和Windows操作系统在底层架构上存在较大差异。不过,有一些方法可以让Android手机...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
