pnpm 教程:从入门到精通
目录
- 引言:为什么选择 pnpm?
- Node.js 包管理器的演进
- pnpm 的核心优势
- pnpm 快速入门
- 安装 pnpm
- 初始化新项目
- 安装依赖
- 运行脚本
- pnpm 的工作原理
- 内容寻址存储(Content-addressable store)
- 硬链接与软链接
- pnpm 的
node_modules结构
- 进阶使用
- Monorepo 管理
pnpm-workspace.yaml配置- 工作区命令 (
pnpm -F) - 包间依赖管理
- 版本管理与锁定
pnpm-lock.yaml- 更新依赖
- 指定版本安装
- 缓存管理
- 清除缓存
- 查看缓存位置
- 配置管理
.npmrc与.pnpmrc- 常用配置项
- Monorepo 管理
- pnpm 常用命令速查
- 最佳实践与注意事项
- 在 CI/CD 中使用 pnpm
- 与 Yarn/npm 迁移
- 解决常见问题
- 总结
1. 引言:为什么选择 pnpm?
Node.js 包管理器的演进
在 Node.js 生态系统中,包管理器经历了从 npm 到 Yarn 再到 pnpm 的演进。
* npm (Node Package Manager):作为官方的包管理器,npm 解决了 JavaScript 模块复用的问题。然而,早期的 npm 存在依赖地狱、安装速度慢、占用磁盘空间大等问题。
* Yarn:为了解决 npm 的痛点,Facebook 推出了 Yarn。它带来了更快的安装速度、确定性依赖 (yarn.lock) 和离线安装等特性,极大地提升了开发体验。
* pnpm (Performant npm):pnpm 在 Yarn 的基础上,进一步优化了磁盘空间占用和安装速度,并引入了更严格的依赖管理机制,尤其在 Monorepo 项目中表现出色。
pnpm 的核心优势
- 极速安装:pnpm 的安装速度通常比 npm 和 Yarn 更快,因为它采用了一种独特的存储方式。
- 节省磁盘空间:这是 pnpm 最显著的优势。它通过内容寻址的存储方式,避免了重复安装相同版本的依赖,大大减少了磁盘占用。
- 严格的依赖管理:pnpm 创建了一个非扁平化的
node_modules结构,这使得项目只能访问其直接声明的依赖,而非传递性依赖。这有助于防止“幻影依赖”问题(Phantom Dependencies),使依赖关系更清晰、更可预测。 - Monorepo 友好:pnpm 对 Monorepo(多包仓库)项目提供了原生支持,能够高效管理不同子包之间的依赖关系,共享相同的依赖实例。
- 确定性安装:
pnpm-lock.yaml文件确保了每次安装都会得到完全相同的依赖树。
2. pnpm 快速入门
安装 pnpm
你可以通过 npm 或 cURL (适用于 macOS/Linux) 安装 pnpm。
使用 npm 安装(推荐,确保你的 npm 版本较新):
bash
npm install -g pnpm
使用 cURL (macOS/Linux):
bash
curl -fsSL https://get.pnpm.io/install.sh | sh
安装完成后,可以通过以下命令验证安装:
bash
pnpm -v
初始化新项目
进入你的项目目录,然后运行:
bash
pnpm init
这会创建一个 package.json 文件。
安装依赖
安装项目依赖就像 npm 或 Yarn 一样简单。
安装普通依赖:
bash
pnpm add lodash
pnpm i axios # i 是 add 的简写
安装开发依赖:
bash
pnpm add -D webpack
pnpm add --save-dev jest
安装全局依赖:
bash
pnpm add -g <package-name>
安装所有依赖(根据 package.json):
bash
pnpm install
pnpm i # i 是 install 的简写
安装完成后,你会发现一个 node_modules 文件夹和一个 pnpm-lock.yaml 文件。
运行脚本
pnpm 可以直接运行 package.json 中 scripts 部分定义的命令。
bash
pnpm start
pnpm test
pnpm build
如果你想执行 node_modules/.bin 中的可执行文件,也无需手动指定路径,pnpm 会自动处理:
bash
pnpm webpack --version
3. pnpm 的工作原理
pnpm 的核心优势来源于其独特的依赖管理机制:内容寻址存储和硬链接/软链接。
内容寻址存储 (Content-addressable store)
pnpm 在你的文件系统上维护了一个全局的“内容寻址存储”。这意味着:
- 无论你在多少个项目中安装了
[email protected],它只会实际存储在磁盘上的一个位置。 - 这个存储目录通常在
~/.pnpm-store或pnpm store path命令显示的路径。
当你在一个项目中安装依赖时,pnpm 会:
- 检查其全局存储中是否已有该版本的包。
- 如果存在,则直接使用存储中的包。
- 如果不存在,则下载该包并将其存储到全局存储中,然后供项目使用。
硬链接与软链接
pnpm 利用操作系统的硬链接(Hard Link)和符号链接(Symbolic Link,也称软链接)来实现其高效的依赖管理。
-
硬链接 (Hard Link):
- 当你项目安装一个包时,pnpm 会在你的项目
node_modules/.pnpm目录下创建一个目录,并通过硬链接将全局存储中的包文件链接到这个目录下。 - 硬链接指向的是文件的实际数据。它不是文件的副本,而是文件的一个额外入口。这意味着无论有多少个硬链接指向同一个文件,它们都共享相同的磁盘空间。删除一个硬链接并不会删除文件本身,除非它是最后一个指向该文件的链接。
- 通过硬链接,项目中的文件实际上是全局存储中文件的“指针”,不占用额外的磁盘空间。
- 当你项目安装一个包时,pnpm 会在你的项目
-
符号链接 / 软链接 (Symbolic Link):
- 在你的项目根目录下的
node_modules中,pnpm 会为每个直接依赖创建一个符号链接。 - 这个符号链接指向
node_modules/.pnpm目录中对应包的硬链接实例。 - 例如,如果你的项目依赖
foo,node_modules/foo将是一个软链接,指向node_modules/.pnpm/[email protected]/node_modules/foo。 node_modules/.pnpm/[email protected]/node_modules/foo内部则通过硬链接指向全局存储中的实际文件。
- 在你的项目根目录下的
pnpm 的 node_modules 结构
传统的 npm 和 Yarn(v1)会创建扁平化的 node_modules 结构,将所有直接和间接依赖都提升到项目的 node_modules 根目录。这可能导致:
- 幻影依赖 (Phantom Dependencies):你的项目代码可以使用未在
package.json中明确声明的传递性依赖。这使得升级或移除某个依赖时可能出现意想不到的错误。 - 幽灵依赖 (Ghost Dependencies):与幻影依赖类似,指那些虽然被安装但没有被直接声明的依赖。
pnpm 的 node_modules 结构则更像是嵌套的,但又通过链接技术优化了磁盘占用。它的结构大致如下:
your-project/
├── node_modules/
│ ├── .pnpm/ # pnpm 独有的目录,存储所有硬链接到全局存储的包实例
│ │ ├── [email protected]/
│ │ │ ├── node_modules/ # 实际的包文件通过硬链接指向全局存储
│ │ │ └── foo/ # foo 的文件都在这里
│ │ │ └── node_modules/ # foo 的依赖会在这里
│ │ │ └── bar/ # foo 依赖的 bar
│ │ └── [email protected]/
│ │ ├── node_modules/
│ │ └── bar/
│ ├── foo -> ./.pnpm/[email protected]/node_modules/foo # 软链接到实际的 foo 包
│ └── bar -> ./.pnpm/[email protected]/node_modules/bar # 软链接到实际的 bar 包
└── package.json
└── pnpm-lock.yaml
关键点:
node_modules根目录下只包含你直接依赖的包的符号链接。- 这些符号链接指向
node_modules/.pnpm内部的目录。 node_modules/.pnpm内部的目录包含了实际的包文件(通过硬链接指向全局存储)及其自身的依赖(也通过符号链接指向node_modules/.pnpm内部的其他包)。
这种结构确保了你的项目代码只能访问直接依赖,传递性依赖则嵌套在其父级依赖的 node_modules 中,从而解决了幻影依赖问题,使依赖关系更加清晰和安全。
4. 进阶使用
Monorepo 管理
Monorepo 是 pnpm 的一个亮点功能。它允许你在一个 Git 仓库中管理多个项目(包),并高效地处理它们之间的依赖。
配置 pnpm-workspace.yaml:
在 Monorepo 的根目录下创建 pnpm-workspace.yaml 文件来定义工作区:
“`yaml
pnpm-workspace.yaml
packages:
# 所有在 packages/ 目录下的子目录都被视为工作区包
– ‘packages/‘
# 特定的应用程序包
– ‘apps/‘
# 排除 example 目录
– ‘!packages/example’
“`
packages字段是一个 glob 模式数组,用于指定哪些目录包含工作区中的包。
工作区命令 (pnpm -F)
在 Monorepo 中,你可以使用 -F 或 --filter 标志来过滤要执行命令的包。
-
在所有包中安装依赖:
bash
pnpm install
这会在所有工作区包中安装它们各自的依赖,并处理包间依赖。 -
在特定包中运行脚本:
bash
pnpm -F <package-name> build
pnpm --filter my-app dev -
在所有包中运行脚本:
bash
pnpm -r build # -r 是 --recursive 的简写 -
过滤特定包及其依赖项:
“`bash
在 my-app 及其所有依赖项中运行测试
pnpm -F “my-app…” test
在 my-lib 及其所有反向依赖项(依赖 my-lib 的包)中运行构建
pnpm -F “…my-lib” build
“` -
添加工作区内部依赖:
假设
my-app依赖my-ui-lib(它们都在同一个工作区)。bash
cd apps/my-app
pnpm add my-ui-lib --workspace
--workspace标志告诉 pnpm 安装工作区内部的my-ui-lib,而不是从 npm registry 下载。这会在my-app/package.json中添加"my-ui-lib": "workspace:^1.0.0"这样的依赖。
版本管理与锁定
pnpm-lock.yaml:
与 package-lock.json 或 yarn.lock 类似,pnpm-lock.yaml 文件记录了项目中每个依赖的确切版本和哈希值,确保了每次安装的确定性。
更新依赖:
-
更新所有依赖到最新版本(遵循
package.json中的版本范围):bash
pnpm update -
更新特定依赖:
bash
pnpm update lodash -
更新到最新主要版本(major version):
bash
pnpm update -L # 或 --latest
指定版本安装:
bash
pnpm add [email protected]
pnpm add some-package@next # 安装 pre-release 版本
缓存管理
pnpm 维护了一个本地缓存,用于存储下载的包压缩文件,以加快未来的安装速度。
-
清除缓存:
bash
pnpm store prune # 移除不再被任何项目引用的包
pnpm store status # 检查存储中的数据完整性
pnpm store path # 显示存储的路径 -
清理所有未使用的包数据:
为了彻底清理,你可能需要手动删除pnpm store path指向的目录。
注意: 这会删除所有已缓存的包,下次安装需要重新下载。
配置管理
pnpm 的配置可以通过 .npmrc 或 .pnpmrc 文件进行。
-
.npmrc(推荐):
pnpm 兼容 npm 的.npmrc文件。你可以在项目根目录或用户主目录下创建它。 -
.pnpmrc:
pnpm 也有自己的.pnpmrc文件,通常用于 pnpm 特有的配置。
常用配置项:
-
registry: 设置 npm registry 地址。“`
.npmrc
registry=https://registry.npmmirror.com/
“` -
shamefully-hoist: 如果你遇到一些遗留项目或工具无法适应 pnpm 的非扁平node_modules结构,可以启用这个选项。它会创建更扁平的node_modules,类似于 npm/Yarn 的行为,但会失去 pnpm 的严格性优势。不推荐在 Monorepo 或新项目中使用。“`
.pnpmrc
shamefully-hoist=true
“` -
public-hoist-pattern: 当shamefully-hoist为false时,可以指定哪些包可以被提升到根node_modules。“`
.pnpmrc
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
“` -
strict-peer-dependencies: 严格检查 peer dependencies。默认为true。“`
.pnpmrc
strict-peer-dependencies=false
“`
5. pnpm 常用命令速查
| 命令 | 描述 | 示例 |
|---|---|---|
pnpm init |
初始化一个新项目 | pnpm init |
pnpm install |
安装所有依赖 | pnpm i |
pnpm add <pkg> |
安装一个包作为依赖 | pnpm add lodash |
pnpm add -D <pkg> |
安装一个包作为开发依赖 | pnpm add -D webpack |
pnpm add -g <pkg> |
全局安装一个包 | pnpm add -g ts-node |
pnpm remove <pkg> |
卸载一个包 | pnpm rm lodash |
pnpm update |
更新所有依赖 | pnpm up |
pnpm update <pkg> |
更新指定包 | pnpm up lodash |
pnpm up -L |
更新所有依赖到最新主要版本 | pnpm up --latest |
pnpm link |
将本地包链接到全局存储 | pnpm link |
pnpm unlink |
移除全局链接 | pnpm unlink |
pnpm run <script> |
运行 package.json 中的脚本 |
pnpm run start |
pnpm test |
运行 test 脚本 (快捷方式) |
pnpm test |
pnpm start |
运行 start 脚本 (快捷方式) |
pnpm start |
pnpm publish |
发布包到 npm registry | pnpm publish |
pnpm root |
显示 node_modules 根目录 |
pnpm root |
pnpm store path |
显示全局内容寻址存储路径 | pnpm store path |
pnpm store prune |
清理存储中未引用的包 | pnpm store prune |
pnpm doctor |
检查 pnpm 环境和依赖问题 | pnpm doctor |
pnpm -F <pkg> <cmd> |
在 Monorepo 中过滤并执行命令 | pnpm -F my-app build |
pnpm -r <cmd> |
在 Monorepo 所有包中递归执行命令 | pnpm -r test |
pnpm config set <key> <value> |
设置配置项 | pnpm config set registry https://registry.npm.taobao.org/ |
pnpm exec <cmd> |
执行 node_modules/.bin 中的命令 |
pnpm exec webpack |
6. 最佳实践与注意事项
在 CI/CD 中使用 pnpm
在持续集成/持续部署 (CI/CD) 流程中使用 pnpm 可以显著提高效率。
- 安装 pnpm: 确保你的 CI/CD 环境中安装了 pnpm。许多 CI/CD 服务(如 GitHub Actions)都有专门的 pnpm setup action。
“`yaml
# GitHub Actions 示例- uses: pnpm/action-setup@v2
with:
version: 8.x # 指定 pnpm 版本 - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: ‘pnpm’ # 启用 pnpm 缓存
“`
- uses: pnpm/action-setup@v2
- 安装依赖: 使用
pnpm install --frozen-lockfile。--frozen-lockfile选项会强制 pnpm 严格按照pnpm-lock.yaml文件安装依赖,如果package.json和pnpm-lock.yaml不一致,则会报错,从而保证构建的确定性。 - 缓存
pnpm store和node_modules: 在 CI/CD 中缓存 pnpm 的全局存储和node_modules可以大幅缩短构建时间。
与 Yarn/npm 迁移
从 npm 或 Yarn 迁移到 pnpm 通常很简单:
-
删除旧的锁文件和
node_modules:bash
rm -rf node_modules
rm package-lock.json yarn.lock
2. 安装 pnpm: 如果尚未安装。
3. 运行pnpm install: 这会根据package.json生成新的pnpm-lock.yaml。bash
pnpm install
在 Monorepo 项目中,你需要先配置pnpm-workspace.yaml。
解决常见问题
- 找不到模块 / 幻影依赖问题:
如果你从 npm/Yarn 迁移过来,可能会遇到某些在之前可以运行的代码现在找不到模块的问题。这通常是因为你的代码依赖了传递性依赖(Phantom Dependencies)。
解决方案: 明确地将这些传递性依赖添加到你的package.json中作为直接依赖。pnpm 的严格模式鼓励你清晰地声明所有使用的依赖。 - 与其他工具集成问题:
某些旧工具可能对非扁平的node_modules结构支持不好。
解决方案:- 首先尝试更新这些工具到最新版本,它们可能已经支持 pnpm。
- 如果无法更新或仍然存在问题,可以尝试在项目根目录的
.pnpmrc中设置shamefully-hoist=true。但这会牺牲 pnpm 的严格依赖管理优势,应作为最后的手段。
- 权限问题:
全局存储或node_modules可能会遇到权限问题。
解决方案: 检查目录权限。如果是全局安装 pnpm,确保~/.pnpm-store目录对当前用户可写。
7. 总结
pnpm 以其独特的存储和链接机制,为 Node.js 包管理带来了革命性的改进。它在安装速度、磁盘空间利用率和依赖严格性方面表现卓越,尤其在 Monorepo 项目中,能够显著提升开发效率和项目稳定性。
通过掌握 pnpm 的核心概念和常用命令,你将能够更好地管理项目依赖,优化开发工作流,并避免常见的依赖问题。随着前端工程化和大型项目管理的趋势,pnpm 正在成为越来越多开发者的首选包管理器。尝试 pnpm,体验它带来的高效和便捷吧!