pnpm 教程:从入门到精通 – wiki词典


pnpm 教程:从入门到精通

目录

  1. 引言:为什么选择 pnpm?
    • Node.js 包管理器的演进
    • pnpm 的核心优势
  2. pnpm 快速入门
    • 安装 pnpm
    • 初始化新项目
    • 安装依赖
    • 运行脚本
  3. pnpm 的工作原理
    • 内容寻址存储(Content-addressable store)
    • 硬链接与软链接
    • pnpm 的 node_modules 结构
  4. 进阶使用
    • Monorepo 管理
      • pnpm-workspace.yaml 配置
      • 工作区命令 (pnpm -F)
      • 包间依赖管理
    • 版本管理与锁定
      • pnpm-lock.yaml
      • 更新依赖
      • 指定版本安装
    • 缓存管理
      • 清除缓存
      • 查看缓存位置
    • 配置管理
      • .npmrc.pnpmrc
      • 常用配置项
  5. pnpm 常用命令速查
  6. 最佳实践与注意事项
    • 在 CI/CD 中使用 pnpm
    • 与 Yarn/npm 迁移
    • 解决常见问题
  7. 总结

1. 引言:为什么选择 pnpm?

Node.js 包管理器的演进

在 Node.js 生态系统中,包管理器经历了从 npmYarn 再到 pnpm 的演进。
* npm (Node Package Manager):作为官方的包管理器,npm 解决了 JavaScript 模块复用的问题。然而,早期的 npm 存在依赖地狱、安装速度慢、占用磁盘空间大等问题。
* Yarn:为了解决 npm 的痛点,Facebook 推出了 Yarn。它带来了更快的安装速度、确定性依赖 (yarn.lock) 和离线安装等特性,极大地提升了开发体验。
* pnpm (Performant npm):pnpm 在 Yarn 的基础上,进一步优化了磁盘空间占用和安装速度,并引入了更严格的依赖管理机制,尤其在 Monorepo 项目中表现出色。

pnpm 的核心优势

  1. 极速安装:pnpm 的安装速度通常比 npm 和 Yarn 更快,因为它采用了一种独特的存储方式。
  2. 节省磁盘空间:这是 pnpm 最显著的优势。它通过内容寻址的存储方式,避免了重复安装相同版本的依赖,大大减少了磁盘占用。
  3. 严格的依赖管理:pnpm 创建了一个非扁平化的 node_modules 结构,这使得项目只能访问其直接声明的依赖,而非传递性依赖。这有助于防止“幻影依赖”问题(Phantom Dependencies),使依赖关系更清晰、更可预测。
  4. Monorepo 友好:pnpm 对 Monorepo(多包仓库)项目提供了原生支持,能够高效管理不同子包之间的依赖关系,共享相同的依赖实例。
  5. 确定性安装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.jsonscripts 部分定义的命令。

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-storepnpm store path 命令显示的路径。

当你在一个项目中安装依赖时,pnpm 会:

  1. 检查其全局存储中是否已有该版本的包。
  2. 如果存在,则直接使用存储中的包。
  3. 如果不存在,则下载该包并将其存储到全局存储中,然后供项目使用。

硬链接与软链接

pnpm 利用操作系统的硬链接(Hard Link)和符号链接(Symbolic Link,也称软链接)来实现其高效的依赖管理。

  1. 硬链接 (Hard Link)

    • 当你项目安装一个包时,pnpm 会在你的项目 node_modules/.pnpm 目录下创建一个目录,并通过硬链接将全局存储中的包文件链接到这个目录下。
    • 硬链接指向的是文件的实际数据。它不是文件的副本,而是文件的一个额外入口。这意味着无论有多少个硬链接指向同一个文件,它们都共享相同的磁盘空间。删除一个硬链接并不会删除文件本身,除非它是最后一个指向该文件的链接。
    • 通过硬链接,项目中的文件实际上是全局存储中文件的“指针”,不占用额外的磁盘空间。
  2. 符号链接 / 软链接 (Symbolic Link)

    • 在你的项目根目录下的 node_modules 中,pnpm 会为每个直接依赖创建一个符号链接
    • 这个符号链接指向 node_modules/.pnpm 目录中对应包的硬链接实例。
    • 例如,如果你的项目依赖 foonode_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.jsonyarn.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-hoistfalse 时,可以指定哪些包可以被提升到根 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 可以显著提高效率。

  1. 安装 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 缓存
      “`
  2. 安装依赖: 使用 pnpm install --frozen-lockfile--frozen-lockfile 选项会强制 pnpm 严格按照 pnpm-lock.yaml 文件安装依赖,如果 package.jsonpnpm-lock.yaml 不一致,则会报错,从而保证构建的确定性。
  3. 缓存 pnpm storenode_modules 在 CI/CD 中缓存 pnpm 的全局存储和 node_modules 可以大幅缩短构建时间。

与 Yarn/npm 迁移

从 npm 或 Yarn 迁移到 pnpm 通常很简单:

  1. 删除旧的锁文件和 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 结构支持不好。
    解决方案:

    1. 首先尝试更新这些工具到最新版本,它们可能已经支持 pnpm。
    2. 如果无法更新或仍然存在问题,可以尝试在项目根目录的 .pnpmrc 中设置 shamefully-hoist=true。但这会牺牲 pnpm 的严格依赖管理优势,应作为最后的手段。
  • 权限问题:
    全局存储或 node_modules 可能会遇到权限问题。
    解决方案: 检查目录权限。如果是全局安装 pnpm,确保 ~/.pnpm-store 目录对当前用户可写。

7. 总结

pnpm 以其独特的存储和链接机制,为 Node.js 包管理带来了革命性的改进。它在安装速度、磁盘空间利用率和依赖严格性方面表现卓越,尤其在 Monorepo 项目中,能够显著提升开发效率和项目稳定性。

通过掌握 pnpm 的核心概念和常用命令,你将能够更好地管理项目依赖,优化开发工作流,并避免常见的依赖问题。随着前端工程化和大型项目管理的趋势,pnpm 正在成为越来越多开发者的首选包管理器。尝试 pnpm,体验它带来的高效和便捷吧!


滚动至顶部