5月28日 02:12

什么是 pnpm,它与 npm 和 Yarn 有什么区别?

pnpm 是什么

pnpm(Performant npm)是 Node.js 的包管理工具,核心设计目标是通过内容寻址存储解决 npm/Yarn 的磁盘浪费和幽灵依赖问题。

存储机制:内容寻址 vs 扁平复制

npm 和 Yarn classic 采用扁平化安装:每个项目的 node_modules 都复制一份完整的包文件。10 个项目用 lodash,磁盘上就有 10 份副本。

pnpm 的做法不同——所有包只存一份到全局 store(通常在 ~/.local/share/pnpm/store),项目中通过硬链接指向 store 中的文件:

bash
# 查看全局 store 路径 pnpm store path # /home/user/.local/share/pnpm/store/v3 # 查看 store 中已缓存的包 pnpm store status

安装同一个包 10 次,磁盘上只有 1 份数据,项目中的 node_modules 只是硬链接指针。这就是 pnpm 能节省 70%+ 磁盘空间的原因。

node_modules 结构:严格隔离 vs 幽灵依赖

npm/Yarn 的扁平化结构会产生幽灵依赖——你可以在代码中 import 一个没有写在 package.json 里的包,因为它被其他依赖提升(hoisting)到了顶层。

js
// 你的 package.json 只声明了 express // 但 express 依赖了 body-parser,body-parser 又依赖了 qs // npm 下这段代码能运行,因为 qs 被提升到了顶层 const qs = require("qs") // 危险:没有在 package.json 中声明 // 一天 express 换了依赖不再安装 qs,你的代码就崩了

pnpm 的 node_modules 结构是这样的:

shell
node_modules/ ├── .pnpm/ │ ├── express@4.18.2/ │ │ └── node_modules/ │ │ ├── express/ → 硬链接到 store │ │ └── body-parser/ → express 的依赖,只 express 能访问 │ └── qs@6.11.0/ │ └── node_modules/ │ └── qs/ → 硬链接到 store └── express/ → 软链接到 .pnpm/express@4.18.2/node_modules/express

每个包只能访问自己声明的依赖,require("qs") 如果没写在 package.json 里会直接报错。

如果遇到必须用扁平结构的兼容性问题,可以在 .npmrc 中设置:

ini
shamefully-hoist=true

但这会失去严格隔离的优势,只作为最后的兜底方案。

安装速度对比

冷安装(无缓存)和热安装(有缓存)的实际表现:

场景npmYarnpnpm
冷安装(1500 依赖)~48s~22s~14s
热安装(已有缓存)~12s~3s~3s
更新单个依赖~9s~1.2s~0.9s

热安装快的原因:pnpm 检测到 store 中已有该包,直接创建硬链接,不需要网络请求和文件复制。

monorepo 支持

pnpm 原生支持 workspace,通过 pnpm-workspace.yaml 配置:

yaml
# pnpm-workspace.yaml packages: - "apps/*" - "packages/*"
bash
# 只安装某个 workspace 的依赖 pnpm install --filter @myapp/web # 在所有 workspace 中执行脚本 pnpm -r run build # 包间依赖引用 pnpm add @myapp/utils --filter @myapp/web

相比 npm workspaces,pnpm 的 --filter 语法更灵活,且 workspace 间的依赖也遵循严格隔离,不会意外访问兄弟包的内部模块。

迁移到 pnpm

bash
# 从 npm 迁移:导入 lockfile pnpm import # 自动读取 package-lock.json 生成 pnpm-lock.yaml # 或直接安装 rm -rf node_modules package-lock.json pnpm install # 从 Yarn 迁移同理,pnpm import 也支持 yarn.lock

追问

Q: pnpm 的硬链接在跨文件系统时会失效吗?

会。硬链接要求源文件和目标在同一个文件系统分区。如果全局 store 和项目不在同一分区(比如 store 在 SSD,项目在机械硬盘),pnpm 会回退到复制文件,磁盘节省效果消失。Docker 容器中挂载卷时也需注意这个问题。

Q: .npmrcshamefully-hoist=truehoist-pattern[] 怎么选?

优先用 hoist-pattern 做精确控制,只提升特定包到顶层:

ini
# 只提升 eslint 相关包 hoist-pattern[]=eslint* hoist-pattern[]=@eslint/*

shamefully-hoist=true 是全部提升,等于放弃严格隔离,只在遇到无法绕过的兼容性问题时使用。

标签:PNPM