首页 / Node.js 教程 / pnpm 与包管理进阶

Node.js 教程

pnpm 与包管理进阶

本教程共 76 篇 · 第 10 篇 · 更新于 2026-07-25 · 约 7 分钟阅读

Node.jspnpmworkspacemonorepo包管理

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 几乎一一对应,迁移成本极低:

npmpnpm说明
npm installpnpm 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>临时执行远程包
Note

pnpm 的 pnpm <script>npm run <script> 少打几个字,因为 pnpm 会先检查 package.jsonscripts,找不到再当成系统命令。这个设计挺贴心。

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 怎么选

维度npmYarnpnpm
磁盘占用高(每个项目独立拷贝)高(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 用法。