pnpm 与包管理进阶
本教程共 76 篇 · 第 10 篇 · 更新于 2026-07-25 · 约 7 分钟阅读
10. pnpm 与包管理进阶
本节目标:pnpm 的硬链接机制、workspace 单仓多包,以及与 npm、yarn 的对比。
npm 是 Node.js 的官方包管理器,但它不是唯一选择。随着项目变大、node_modules 越来越臃肿,社区里出现了几个替代方案。其中,pnpm 是我目前最推荐的进阶工具。
这一章我们来聊聊:pnpm 为什么更快、更省空间;它的硬链接机制到底是怎么回事;以及怎么用它管理 Monorepo 项目。
传统包管理器的痛点
先说说 npm 和 Yarn 的老问题,这样你才能理解 pnpm 好在哪里。
假设你有 10 个项目,每个都依赖 lodash@4.17.21。用 npm 安装,lodash 的完整代码会在你硬盘上出现 10 次,分别躺在 10 个 node_modules 里。按每个包 5MB 算,就是 50MB 的重复数据。
这还不算什么。真正让人头疼的是「幻影依赖(Phantom Dependencies)」:npm v3 之后采用扁平化结构,把子依赖提到顶层。你的代码里可以 require('a'),哪怕 a 只是 b 的子依赖、你并没有直接安装它。这看起来方便,实则埋雷——一旦 b 升级不再依赖 a,你的代码就直接报错。
pnpm 的设计就是冲着解决这些问题来的。
pnpm 的核心:内容可寻址存储 + 硬链接
pnpm 的全称是 Performant NPM,但它的快不是来自什么黑魔法,而是一个聪明的存储策略。
第一步:全局唯一存储
pnpm 在你的系统里维护一个全局仓库(默认在 ~/.pnpm-store)。当你第一次安装 lodash@4.17.21,它的文件被解压到这个仓库。之后无论多少个项目需要同一个版本的 lodash,pnpm 都不会重新下载。
第二步:硬链接到项目
项目的 node_modules 里,pnpm 不会复制文件,而是创建「硬链接」。硬链接和原文件指向磁盘上的同一份数据,不占额外空间。你可以把它理解成:10 个项目共用一份 lodash,但每个项目都以为自己有一份独立的拷贝。
第三步:符号链接组织依赖树
为了不破坏 Node.js 的模块解析算法,pnpm 会在 node_modules 里用符号链接(软链接)搭出严格的依赖树结构。你的直接依赖平铺在 node_modules 根目录,而子依赖嵌套在 .pnpm 目录里,通过链接关联。
这种结构的副作用是:幻影依赖被彻底消除。你能 require 的包,必须是你 package.json 里明确声明的。代码更严谨,意外也更少。
Tip因为硬链接的存在,pnpm 安装的
node_modules里的文件是只读的。这防止了项目脚本意外修改依赖内容,是个安全加分项。
安装与基本命令
pnpm 本身也是用 npm 装的:
npm install -g pnpm
pnpm -v
基本命令和 npm 几乎一一对应,迁移成本极低:
| npm | pnpm | 说明 |
|---|---|---|
npm install | pnpm install | 安装所有依赖 |
npm install <pkg> | pnpm add <pkg> | 添加依赖 |
npm install -D <pkg> | pnpm add -D <pkg> | 添加开发依赖 |
npm uninstall <pkg> | pnpm remove <pkg> | 移除依赖 |
npm run <script> | pnpm <script> | 运行脚本(pnpm 更简洁) |
npx <cmd> | pnpm dlx <cmd> | 临时执行远程包 |
Notepnpm 的
pnpm <script>比npm run <script>少打几个字,因为 pnpm 会先检查package.json的scripts,找不到再当成系统命令。这个设计挺贴心。
Monorepo:一个仓库管理多个包
当你的项目拆成多个子包——比如一个框架加若干插件,或者前端、后端、共享库放一起——Monorepo 就成了刚需。pnpm 内置的 Workspace 功能,能优雅地解决这个问题。
初始化工作区
在项目根目录创建 pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'apps/*'
目录结构可能是这样:
my-project/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│ ├── utils/
│ │ └── package.json
│ └── ui/
│ └── package.json
└── apps/
├── web/
│ └── package.json
└── api/
└── package.json
安装与依赖管理
在根目录执行 pnpm install,pnpm 会递归给所有子包装依赖,并把公共依赖提升到根目录共享。
如果 apps/web 需要依赖 packages/utils,不用发 npm 包,直接用 Workspace 协议:
// apps/web/package.json
{
"dependencies": {
"@myproject/utils": "workspace:*"
}
}
workspace:* 表示「用当前工作区里的最新版本」。代码一改,依赖方立刻感知,开发体验非常流畅。
批量操作
# 给所有子包执行 build
pnpm -r run build
# 只给某个子包加依赖
pnpm --filter @myproject/web add lodash
# 查看工作区依赖树
pnpm list -r
pnpm 独有的实用功能
store prune:清理磁盘
全局仓库里的包不会被自动删除。如果你装了很多实验性依赖,可以用这个命令回收空间:
pnpm store prune # 删除没有被任何项目引用的包
pnpm store path # 查看全局仓库位置
patch:临时修改依赖
偶尔你需要改某个 npm 包的源码来应急。pnpm 提供了官方支持的 patch 流程:
pnpm patch lodash@4.17.21
# 编辑器会打开一个临时目录,修改源码
pnpm patch-commit /path/to/temp/dir
修改会被记录为 .patch 文件,提交到 Git。团队成员下次 pnpm install 时自动应用,不用手动改 node_modules。
shamefully-hoist:兼容老顽固
有些工具(比如某些 Vue CLI 插件、Electron 构建脚本)硬编码了扁平化 node_modules 的假设,在 pnpm 的严格结构下会报错。可以在 .npmrc 里开这个兼容模式:
shamefully-hoist=true
开启后,pnpm 会把依赖尽量提到顶层,模拟 npm 的扁平化行为。这是折中方案,能不用就不用。
npm、Yarn、pnpm 怎么选
| 维度 | npm | Yarn | pnpm |
|---|---|---|---|
| 磁盘占用 | 高(每个项目独立拷贝) | 高(v1)/ 中(v2+ PnP) | 极低(全局共享+硬链接) |
| 安装速度 | 中等 | 快 | 最快 |
| Monorepo | 支持(v7+ workspaces) | 原生支持 | 原生支持,性能最强 |
| 兼容性 | 基准 | 良好 | 良好,少数工具需适配 |
| 特色功能 | 官方标配 | Plug’n’Play | 严格依赖隔离、patch、store 管理 |
我的建议:
- 小型项目、快速原型:npm 完全够用,不用折腾。
- 中大型项目、多项目共用机器:切 pnpm,磁盘和速度收益明显。
- Monorepo 项目:pnpm Workspace 是目前体验最好的方案之一,比 Yarn Workspaces 快,比 npm workspaces 稳。
迁移:从 npm 切到 pnpm
现有项目想切 pnpm,步骤很简单:
# 1. 安装 pnpm
npm install -g pnpm
# 2. 删除旧的依赖和锁文件
rm -rf node_modules package-lock.json
# 3. 用 pnpm 重新安装(会读取 package.json)
pnpm install
# 4. 生成 pnpm-lock.yaml,提交到 Git
git add pnpm-lock.yaml
Warning团队成员要统一包管理器。混用 npm 和 pnpm 会导致锁文件不同步,CI 环境也会乱。建议在
package.json里加"packageManager": "pnpm@9.0.0",配合 Corepack(Node.js v16.13+ 内置)自动锁定版本。
实战:搭一个 pnpm Monorepo
最后动手搭一个最小可运行的 Monorepo:
mkdir pnpm-mono && cd pnpm-mono
# pnpm-workspace.yaml
packages:
- 'packages/*'
// package.json(根目录,private 防止误发布)
{
"name": "pnpm-mono",
"private": true,
"scripts": {
"build": "pnpm -r run build"
}
}
mkdir -p packages/math packages/app
// packages/math/package.json
{
"name": "@mono/math",
"version": "1.0.0",
"type": "module",
"main": "index.js"
}
// packages/math/index.js
export const add = (a, b) => a + b;
// packages/app/package.json
{
"name": "@mono/app",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@mono/math": "workspace:*"
}
}
// packages/app/index.js
import { add } from '@mono/math';
console.log(add(2, 3));
pnpm install
node packages/app/index.js # 输出 5
整个流程跑通,你就掌握了 pnpm 最核心的 Workspace 用法。