pnpm咋这么好用
2025-12-126 min
为什么使用pnpm
使用 npm 时,依赖每次被不同的项目使用,都会重复安装一次(安装到项目中的独立的node_modules中)。 而在使用 pnpm 时,依赖会被存储在**内容可寻址的存储**(content-addressable store)中,默认是~/.pnpm-store
优点
- 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有 100 个文件,而它的新版本只改变了其中 1 个文件。那么
pnpm update时只会向存储中额外添加 1 个新文件,而不会因为单个改变克隆整个依赖。 - 所有文件都会存储在硬盘上的某一位置。 文件会
**硬链接**到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。- 这是一种缓存机制,yarn也有这种缓存机制,但是是通过复制文件实现的。
这不仅节省了磁盘空间,而且安装速度会很快(reused dependency)
安装的三个阶段
- 依赖解析(resolve,fetching):如果需要的依赖文件不在存储仓库中,则下载到仓库
- 计算node_modules结构
- 硬链接(link)依赖到项目仓库
而且这比传统的三阶段更快
传统的三阶段:解析依赖,获取所有依赖,写入所有依赖到项目目录


非扁平node_modules
传统的扁平化:使用 npm 或 Yarn Classic 安装依赖项时,所有的包都被提升到模块目录的根目录。 这样就导致了一个问题,源码可以直接访问和修改依赖,而不是作为只读的项目依赖。
而pnpm使用符号链接将项目的直接依赖项添加到模块目录的根目录中
扁平化结构的问题:
- 模块可访问非依赖的包;(幽灵依赖)
- 依赖树扁平化算法复杂;
- 部分包需复制到项目node_modules文件夹中;(磁盘操作耗时)
- 未解决磁盘空间占用过高的问题。
npm@3之前非扁平化带来的问题
- 依赖过深,路径过长(可能遇到Windows的长路径限制)
- 多个包重复被复制
pnpm借鉴npm@2的非扁平优点:
- node_modules结构可预测、整洁,每个依赖的node_modules中包含自身依赖
符号链接工作原理
- Node.js 会忽略符号链接、执行真实路径;包的依赖存储在其目录上一级的node_modules中,Node.js 会沿目录结构向上查找依赖,确保包能正常引用所需模块。
例如:
require('foo') 会执行 node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/foo/index.js 而不是node_modules/foo/index.js.,但是在文件路劲表现上,又缩短了直接执行文件的路径长度。
此外,还一定解决了幽灵依赖的问题,因为依赖的依赖不会被提升到node_modules的浅层,只有我们真正需要的依赖才会创建符号链接到node_modules/下
实际的依赖一般在node_modules/.pnpm/下,保持真正的非扁平依赖结构,结构一般为.pnpm/<name>@<version>/node_modules/<name>,而这里的依赖又是链接了全局的可寻址存储的缓存。
需要注意的是,依赖包的依赖和依赖包的实际位置是处于同一级目录的

补充:对等依赖的情况
假设foo@1.0.0有两个 “对等依赖”,分别是bar@^1和baz@^1(意思是它需要用父级项目里的bar和baz,而且版本得是 1.x 系列)。现在项目里有两个 “父包”:
- 第一个父包foo-parent-1,自己装了bar@1.0.0和baz@1.0.0,然后依赖foo@1.0.0;
- 第二个父包foo-parent-2,自己装了bar@1.0.0但baz@1.1.0,也依赖foo@1.0.0。
这时候foo@1.0.0就尴尬了:跟着第一个父包,得用baz@1.0.0;跟着第二个父包,又得用baz@1.1.0—— 相当于一个人要适配两套不同的 “配件”,正常的 “一套依赖” 肯定不够用。
对于,没有对等依赖的情况,它应该如下硬链接到其依赖项的符号链接旁边的 node_modules 文件夹:
pnpm 会给同一个包的不同 “对等依赖组合”,创建不同的文件夹。比如刚才的foo@1.0.0,会生成两个文件夹:
- 一个叫
foo@1.0.0_bar@1.0.0+baz@1.0.0:里面的bar目录软链接到bar@1.0.0,baz目录软链接到baz@1.0.0,专门给foo-parent-1用; - 另一个叫
foo@1.0.0_bar@1.0.0+baz@1.1.0:里面的bar还是1.0.0,但baz换成了1.1.0的软链接,给foo-parent-2用。
这样一来,虽然看起来foo@1.0.0多了个 “副本”,但其实只是多了个 “链接文件夹”
如果包依赖了含有对等依赖的子包,则它也会受到影响
局限性
- pnpm 忽略 npm 的 package-lock.json 和 npm-shrinkwrapp.json?
- 核心原因是两者的 node_modules 布局设计逻辑不兼容。npm 的锁文件是为适配其扁平化 node_modules 布局而设计,且 npm 允许同一 name@version 的包多次安装并拥有不同依赖集;而 pnpm默认使用独立的 node_modules 布局,与 npm 锁文件的设计逻辑不匹配,因此无法遵循该格式,只能直接忽略。
- 若项目原本用 npm 管理,且有 package-lock.json,现在要迁移到 pnpm
- pnpm import 命令。该命令的作用是将 npm 的锁文件(如 package-lock.json、npm-shrinkwrapp.json)转换为 pnpm 兼容的锁文件格式,从而实现从 npm 到 pnpm 的锁文件平滑迁移,避免因锁文件不兼容导致的依赖安装问题。
- pnpm 10.x 中 node_modules/.bin 下的 Binstubs 是终端文件而非符号链接,这种设计的目的是什么?
- 目的是解决兼容性问题。pnpm 的 node_modules 采用默认独立布局(非扁平化),部分 “支持插件的 CLI 程序” 可能因路径解析规则变化,无法找到所需插件;将 Binstubs 设计为终端文件,可帮助这类 CLI 程序在 pnpm 的特殊目录结构中正确定位插件,确保其正常运行。
pnpm link
pnpm link <dir>
将 <dir> 目录中的软件包链接到执行此命令的软件包的 node_modules。
例如,如果你在~/projects/foo下,并且执行 pnpm link ../bar,那么将在 foo/node_modules/bar 中创建指向 bar 的链接。
pnpm link <pkg>
将指定的软件包 (<pkg>) 从全局的 node_modules 链接到执行此命令的软件包的node_modules。
pnpm link
将执行此命令的位置的包链接到全局 node_modules,这样它可以通过pnpm link <pkg> 从另一个包中引用。 此外,如果软件包具有 bin 字段,则软件包的二进制文件将在系统范围内可用。
总结就是,不指定名字,就把自己链接到全局,否则就是把其他的命令链接为当前项目可用
pnpm-lock.yaml
pnpm的依赖锁文件
你应该始终提交锁文件(pnpm 生成的 pnpm-lock.yaml 文件)。 这是出于多种原因,主要是:
- 在 CI 和生产环境中能够更快地完成安装,因为解析依赖的过程可以被跳过。
- 开发,测试和生产环境之间强制执行一致的安装和解析方案,这意味着测试和生产中使用的包将与你开发项目时完全相同
pnpm-workspace.yaml
packages
工作空间,monorepo常用
catalogs
将依赖项版本定义为可复用常量。 目录中定义的常量可以在 package.json 文件中引用。
例如刚才的配置
等同于
好处
在工作空间(即 monorepo 或多包 repo)中,许多包使用相同的依赖项是很常见的。 在编写 package.json 文件时,可以减少重复
- 维护唯一版本,易于更新,减少合并冲突
Catalog 协议允许在冒号后使用可选名称 (例如:catalog:name) 来指定应使用哪个目录。 当省略名称时,将使用默认目录。
例如,下面是一种具名目录
和workspace:协议一样,catalog:在发布时也会被替换为正常的依赖版本号
alias别名
pnpm 别名(Aliases)是一种允许用户用自定义名称安装软件包的功能,可在不修改代码引用或项目结构的前提下,灵活替换、管理包版本,解决传统包管理中“版本冲突”“自定义包替换需全局改引用”等问题。
典型使用场景
1. 自定义修复包替换原包(无需改代码)
- 场景描述:项目依赖的
lodash存在 bug,你在分叉库中修复后发布为awesome-lodash,但不想修改项目中所有require('lodash')引用。 - 解决方式:用
lodash作为别名安装awesome-lodash,代码引用无需变动,自动指向修复后的包。 - 指令:
pnpm add lodash@npm:awesome-lodash
2. 同一项目需使用同一包的多个版本
- 场景描述:项目中某模块依赖
lodash@1的旧特性,另一模块需lodash@2的新功能,直接升级会导致旧模块报错。 - 解决方式:给不同版本的
lodash分配自定义别名,分别引用,避免版本冲突。 - 指令:
pnpm add lodash1@npm:lodash@1(安装 v1 并命名为 lodash1)
pnpm add lodash2@npm:lodash@2(安装 v2 并命名为 lodash2) - 引用方式:
require('lodash1')(用 v1)、require('lodash2')(用 v2)
3. 全局替换项目所有依赖中的目标包
- 场景描述:不仅项目自身,项目依赖的其他第三方包也引用了有 bug 的
lodash,需统一替换为修复后的awesome-lodash,避免“局部替换不彻底”。 - 解决方式:通过
.pnpmfile.cjs钩子文件,在读取所有包依赖时,自动将lodash替换为awesome-lodash,实现全局统一替换。 - 关键操作:创建上述钩子文件,pnpm 安装依赖时会自动执行钩子逻辑。
钩子文件示例(.pnpmfile.cjs)
存储配置
过滤、排除
可通过 --filter (或 -F) 标志指定选择器: