第一章:pnpm 入门基础
1.1 什么是 pnpm
| 概念名称 | 说明 | 注意事项 |
|---|
| pnpm | 一个快速、节省磁盘空间的 JavaScript 包管理器,使用硬链接和符号链接在全局 store 中共享依赖,避免重复安装相同包。 | 不兼容某些依赖扁平化 node_modules 的旧工具(如部分 webpack 插件),但现代工具链已普遍支持。 |
| 内容可寻址存储(Content-addressable store) | pnpm 将所有安装过的包统一存放在全局 store(通常为 ~/.pnpm-store),通过内容哈希索引,确保相同版本只存一份。 | store 可跨项目共享,显著减少磁盘占用和安装时间。 |
| 非扁平化 node_modules | pnpm 默认不将依赖提升到顶层 node_modules,而是通过符号链接构建严格的依赖树,防止”幽灵依赖”问题。 | 开发者需确保代码仅引用显式声明的依赖,否则运行时可能报错。 |
1.2 pnpm 与 npm / Yarn 的区别
| 对比维度 | pnpm | npm | Yarn (Classic / Berry) | 注意事项 |
|---|
| 依赖存储方式 | 使用全局 content-addressable store + 硬链接/符号链接 | 每个项目独立下载完整副本(npm v7+ 支持部分 dedupe) | Yarn Classic 类似 npm;Yarn Berry 支持 PnP(无需 node_modules) | pnpm 节省最多磁盘空间,尤其在多项目场景 |
| node_modules 结构 | 非扁平化(严格隔离) | 扁平化(v3+ 自动提升) | Classic 扁平化;Berry 可选 PnP 或 node_modules | pnpm 更安全,避免隐式依赖 |
| 安装速度 | 极快(复用 store) | 中等 | Classic 中等;Berry 快(缓存机制强) | pnpm 在 CI 和大型 monorepo 中优势明显 |
| Monorepo 支持 | 原生支持 workspaces | v7+ 支持 workspaces | 原生支持(Yarn Workspaces) | 三者均支持,但 pnpm 配置更简洁 |
| 幽灵依赖防护 | 强(默认禁止访问未声明依赖) | 弱(扁平化导致可访问) | Classic 弱;Berry 强(PnP 模式) | pnpm 有助于提升项目可维护性 |
1.3 安装 pnpm
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 使用 npm 安装 | npm install -g pnpm | 全局安装 pnpm(最通用方式) | npm install -g pnpm | 需已安装 Node.js 和 npm |
| 使用 Corepack(推荐) | corepack enable | 利用 Node.js 内置 Corepack 管理 pnpm 版本 | corepack enable | Node.js ≥ v16.13 或 ≥ v14.19;无需额外安装 npm |
| corepack prepare pnpm@latest --activate | | corepack prepare pnpm@latest --activate | |
| 使用独立脚本安装 | curl -fsSL https://get.pnpm.io/install.sh | sh - | 适用于无 npm 环境(如 Docker) | curl -fsSL https://get.pnpm.io/install.sh | sh - | 需 bash/sh 环境;自动添加到 PATH |
| 升级 pnpm | pnpm add -g pnpm | 升级已安装的 pnpm 到最新版 | pnpm add -g pnpm | 若通过 Corepack 安装,建议用 corepack prepare pnpm@x.x.x --activate 指定版本 |
1.4 初始化项目与基本命令
| 命令名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 初始化项目 | pnpm init | 创建 package.json 文件 | pnpm init | 交互式填写项目信息,或加 -y 跳过(pnpm init -y) |
| 安装依赖 | pnpm install 或 pnpm i | 根据 package.json 和 pnpm-lock.yaml 安装依赖 | pnpm install | 首次运行会创建 pnpm-lock.yaml 和 node_modules |
| 添加开发依赖 | pnpm add -D <pkg> | 安装包并加入 devDependencies | pnpm add -D typescript | -D 等价于 --save-dev |
| 添加生产依赖 | pnpm add <pkg> | 安装包并加入 dependencies | pnpm add lodash | 默认行为即为生产依赖 |
| 查看 pnpm 版本 | pnpm --version | 检查当前 pnpm 版本 | pnpm --version | 用于确认是否安装成功 |
| 查看帮助 | pnpm help 或 pnpm -h | 显示命令帮助信息 | pnpm help add | 可对具体子命令查看帮助(如 pnpm help install) |
| 列出已安装包 | pnpm list 或 pnpm ls | 显示当前项目依赖树 | pnpm list | --depth=0 仅显示顶层依赖 |
| | | pnpm list --depth=0 | |
第二章:核心功能与命令详解
2.1 添加依赖(add)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 添加生产依赖 | pnpm add <pkg> | 安装包并写入 dependencies | pnpm add lodash | 默认行为;若 package.json 不存在会自动创建 |
| 添加开发依赖 | pnpm add -D <pkg> 或 pnpm add --save-dev <pkg> | 安装包并写入 devDependencies | pnpm add -D typescript | 常用于构建、测试、类型定义等工具 |
| 添加可选依赖 | pnpm add -O <pkg> 或 pnpm add --save-optional <pkg> | 安装包并写入 optionalDependencies | pnpm add -O fsevents | 可选依赖安装失败不会中断整个安装过程 |
| 指定版本或标签 | pnpm add <pkg>@<version> | 安装特定版本或标签(如 latest、beta) | pnpm add react@18.2.0 | 支持语义化版本(semver)、tag、git URL、本地路径等 |
| | | pnpm add vue@next | |
| 从 Git 安装 | pnpm add <git-url> | 从 Git 仓库安装包 | pnpm add git+https://github.com/user/repo.git | 需确保仓库包含有效 package.json |
| | | pnpm add github:user/repo#v1.0.0 | |
| 从本地路径安装 | pnpm add file:../my-lib | 从本地文件系统链接包 | pnpm add file:../shared-utils | 适用于本地开发联调;会创建符号链接 |
| 仅更新 lockfile | pnpm add --lockfile-only <pkg> | 不安装 node_modules,仅更新 pnpm-lock.yaml | pnpm add --lockfile-only axios | 用于 CI 或协作时仅同步依赖声明 |
| 安装到 workspace 子项目 | pnpm -F <workspace-name> add <pkg> | 在 monorepo 中为指定子项目添加依赖 | pnpm -F web add react-router | 需已配置 pnpm-workspace.yaml |
2.2 删除依赖(remove)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 删除依赖 | pnpm remove <pkg> 或 pnpm rm <pkg> | 从 dependencies/devDependencies/optionalDependencies 中移除包并卸载 | pnpm remove lodash | 自动识别依赖类型并清理 package.json 和 node_modules |
| 删除多个依赖 | pnpm remove <pkg1> <pkg2> | 同时删除多个包 | pnpm remove express cors | 所有指定包必须存在,否则报错 |
| 从 workspace 子项目删除 | pnpm -F <workspace-name> remove <pkg> | 在 monorepo 中为指定子项目删除依赖 | pnpm -F api remove axios | 需在根目录执行,且子项目名称匹配 pnpm-workspace.yaml |
| 仅更新 lockfile(不卸载) | pnpm remove --lockfile-only <pkg> | 仅从 lockfile 和 package.json 移除,不操作 node_modules | pnpm remove --lockfile-only debug | 适用于仅同步依赖变更的场景 |
2.3 更新依赖(update)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 更新所有依赖 | pnpm update 或 pnpm up | 根据 package.json 的 semver 范围升级到最新兼容版本 | pnpm update | 不会突破 semver 范围(如 ^1.0.0 不会升到 2.0.0) |
| 更新指定包 | pnpm update <pkg> | 仅更新指定包 | pnpm update react | 同样受 semver 范围限制 |
| 忽略 semver 范围强制升级 | pnpm update --latest | 升级到最新发布版本(可能破坏兼容性) | pnpm update --latest lodash | 会修改 package.json 中的版本号为具体最新版 |
| 交互式选择版本 | pnpm update --interactive | 以交互方式选择要升级的包和版本 | pnpm update --interactive | 需终端支持交互输入;适合谨慎升级 |
| 仅更新 lockfile | pnpm update --lockfile-only | 不安装,仅更新 pnpm-lock.yaml | pnpm update --lockfile-only | 用于协作时同步依赖解析结果 |
| 更新 workspace 子项目依赖 | pnpm -F <name> update <pkg> | 为指定子项目更新依赖 | pnpm -F web update react-dom | 在 monorepo 中常用 |
2.4 安装全部依赖(install)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 安装依赖 | pnpm install 或 pnpm i | 根据 package.json 和 pnpm-lock.yaml 安装依赖 | pnpm install | 若无 lockfile,会生成新 lockfile 并安装最新兼容版本 |
| 强制重新安装 | pnpm install --force | 忽略 store 缓存,重新下载并链接所有依赖 | pnpm install --force | 用于解决依赖损坏或符号链接异常 |
| 仅安装生产依赖 | pnpm install --prod 或 pnpm install --production | 跳过 devDependencies 安装 | pnpm install --prod | 适用于生产环境部署 |
| 安装时忽略脚本 | pnpm install --ignore-scripts | 跳过 preinstall/postinstall 等生命周期脚本 | pnpm install --ignore-scripts | 提高安全性,防止恶意脚本执行 |
| 安装时冻结 lockfile | pnpm install --frozen-lockfile | 若 lockfile 与 package.json 不匹配则报错(不更新) | pnpm install --frozen-lockfile | 推荐在 CI 中使用,确保依赖一致性 |
| 安装 peerDependencies | pnpm install --include-peer-dependencies | 显式安装 peerDependencies(默认不自动安装) | pnpm install --include-peer-dependencies | 某些插件开发场景可能需要 |
2.5 运行脚本(run)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 运行 package.json 脚本 | pnpm run <script-name> | 执行 scripts 中定义的命令 | pnpm run build | 自动将 node_modules/.bin 加入 PATH |
| 列出可用脚本 | pnpm run(无参数) | 显示所有可运行的脚本 | pnpm run | 方便查看项目支持的操作 |
| 传递参数给脚本 | pnpm run <script> -- <args> | 将额外参数传给脚本命令 | pnpm run test -- --watch | -- 后的内容原样传递给底层命令 |
| 并行运行多个脚本 | pnpm dlx concurrently "script1" "script2" | 使用第三方工具并发执行(pnpm 本身不内置并发) | pnpm dlx concurrently "dev:client" "dev:server" | dlx 类似 npx,临时运行包 |
| 运行未安装的包 | pnpm dlx <pkg> | 临时下载并运行包(无需全局安装) | pnpm dlx create-react-app my-app | 替代 npx,使用 pnpm 的 store 和解析逻辑 |
2.6 全局包管理(global)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 全局安装包 | pnpm add -g <pkg> | 安装包到全局,命令可全局使用 | pnpm add -g http-server | 全局 bin 目录需在系统 PATH 中(通常自动配置) |
| 全局卸载包 | pnpm remove -g <pkg> | 卸载全局包 | pnpm remove -g http-server | 同时移除可执行命令 |
| 列出全局包 | pnpm list -g | 查看已安装的全局包 | pnpm list -g --depth=0 | --depth=0 避免显示依赖树 |
| 全局包存储位置 | pnpm root -g | 显示全局 node_modules 路径 | pnpm root -g | 通常为 ~/.pnpm/global |
| 全局运行未安装包 | pnpm dlx <pkg> | 临时运行包(不持久安装) | pnpm dlx cowsay "Hello" | 推荐替代全局安装,避免污染环境 |
| 查看全局 bin 路径 | pnpm bin -g | 显示全局可执行文件目录 | pnpm bin -g | 确保该路径已加入系统 PATH |
第三章:高级特性
3.1 硬链接与符号链接机制
| 概念名称 | 说明 | 注意事项 |
|---|
| 硬链接(Hard Link) | pnpm 将每个包的真实文件存储在全局 store(如 ~/.pnpm-store/v3/files/…)中,项目 node_modules 中的文件通过硬链接指向 store 中的同一 inode。 | 硬链接不占用额外磁盘空间(仅增加目录项),但仅限同一文件系统内使用。 |
| 符号链接(Symbolic Link / Symlink) | pnpm 使用符号链接构建依赖树结构:node_modules/ 是指向 store 中真实包目录的符号链接;嵌套依赖通过多层符号链接解析。 | 符号链接可跨文件系统,但某些旧工具(如部分 Windows 工具链)可能不兼容。 |
| 虚拟 Store 目录(Virtual Store) | 在 node_modules/.pnpm/ 下,pnpm 为每个依赖创建”虚拟目录”,通过符号链接组合出完整的依赖视图,确保模块能正确解析其依赖。 | 开发者不应直接修改 .pnpm 目录;该机制是 pnpm 非扁平化结构的核心。 |
| 内容可寻址(Content-addressable) | store 中的文件路径由包内容的哈希值决定(如 registry.npmjs.org/lodash/4.17.21 → 哈希路径),确保相同内容只存一份。 | 即使不同项目安装同一版本 lodash,也共享同一份物理文件。 |
3.2 node_modules 结构(非扁平化)
| 概念名称 | 说明 | 注意事项 |
|---|
| 非扁平化结构 | pnpm 的 node_modules 不将依赖提升到顶层,而是严格按 package.json 声明的依赖关系组织,每个包只能访问其直接声明的依赖。 | 防止”幽灵依赖”(即未声明却能引用的包),提升项目可维护性与可移植性。 |
| 顶层符号链接 | 项目根 node_modules 下仅包含顶层依赖的符号链接,如 node_modules/lodash → .pnpm/lodash@4.17.21/node_modules/lodash。 | 用户看到的结构简洁,实际依赖解析由 .pnpm 目录完成。 |
| 依赖隔离 | 包 A 和包 B 即使依赖同一子包 C 的不同版本,也不会冲突,因为各自通过独立符号链接指向对应版本。 | 解决了 npm 扁平化导致的”依赖版本冲突”问题。 |
| Node.js 模块解析兼容性 | pnpm 通过精心构造的符号链接结构,使 Node.js 的模块解析算法(如 require())能正常工作。 | 极少数依赖绝对路径或手动遍历 node_modules 的工具可能失效,需适配。 |
3.3 pnpm store 与磁盘节省原理
| 概念名称 | 说明 | 注意事项 |
|---|
| 全局 Store | pnpm 默认将所有下载的包缓存在全局 store(路径可通过 pnpm store path 查看),默认位于 ~/.pnpm-store。 | 多个项目共享同一 store,显著减少重复下载和磁盘占用。 |
| 磁盘节省原理 | 利用硬链接:同一文件在多个项目中通过硬链接引用,物理数据仅存一份,节省 50%~90% 磁盘空间(尤其在 monorepo 场景)。 | 实测:10 个 React 项目共用 react、react-dom 等,总大小接近单个项目 + 少量差异。 |
| Store 清理 | pnpm store prune 可删除未被任何项目引用的 store 文件,释放空间。 | 安全操作,不会影响正在使用的项目。 |
| 自定义 Store 路径 | 通过配置 .npmrc 中的 store-dir=/custom/path 或环境变量 PNPM_HOME 修改 store 位置。 | 适用于磁盘分区管理或 CI 缓存优化。 |
| Store 版本隔离 | v3 与 v4+ 的 store 格式不兼容,升级 pnpm 大版本后会自动创建新 store。 | 旧 store 不会自动删除,可手动清理。 |
3.4 支持 workspaces(monorepo)
| 操作步骤名称 | 操作细节 | 注意事项 |
|---|
| 创建 workspace 配置文件 | 在项目根目录创建 pnpm-workspace.yaml,列出子包路径,如:packages: - 'packages/*' - 'apps/**' | 文件名必须为 pnpm-workspace.yaml(或 .yaml),不可为 JSON。 |
| 子项目结构 | 每个子目录需包含有效的 package.json,且具有唯一 name 字段。 | 子项目可独立发布,也可仅用于内部引用。 |
| 安装所有子项目依赖 | 在根目录运行 pnpm install,pnpm 自动识别 workspace 并链接本地包。 | 本地包之间通过符号链接关联,无需 npm link。 |
| 引用 workspace 包 | 在子项目 A 中执行 pnpm add <子项目B的name>,若 B 在 workspace 中,则自动创建符号链接而非下载远程包。 | 例如:pnpm add @myorg/utils(若 utils 在 workspace 中) |
| 在指定子项目执行命令 | 使用 -F 或 --filter 标志:pnpm -F web run build 或 pnpm --filter ./packages/lib test | 支持通配符(如 *)、路径、标签(如 --filter ...web 表示依赖 web 的包) |
| 发布 workspace 包 | 使用 pnpm -r publish(-r 表示递归),可配合 --filter 限定范围。 | 建议先运行 pnpm -r build 确保产物最新。 |
3.5 .pnpm-debug.log 与故障排查
| 操作步骤名称 | 操作细节 | 注意事项 |
|---|
| 启用调试日志 | 设置环境变量 PNPM_DEBUG=1 或使用 --reporter=ndjson 获取结构化日志。 | 日志默认输出到控制台;错误时自动生成 .pnpm-debug.log。 |
| 查看调试日志文件 | 出错时 pnpm 会在当前目录生成 .pnpm-debug.log,包含完整错误栈、命令参数、Node/pnpm 版本等。 | 文件内容为 NDJSON 格式,可用文本编辑器查看。 |
| 常见错误类型 | 包括:网络超时、权限不足、store 损坏、符号链接失败(Windows)、peerDependencies 冲突等。 | Windows 用户需启用”开发者模式”或以管理员身份运行以支持符号链接。 |
| 清理并重试 | 执行 pnpm install --force 或删除 node_modules + pnpm-lock.yaml 后重装。 | 可解决因符号链接损坏或 lockfile 不一致导致的问题。 |
| 提交 Issue 信息 | 若需向 pnpm 社区报告 bug,应提供:.pnpm-debug.log、pnpm -v、node -v、操作系统、复现步骤。 | 日志中不含敏感信息,可安全分享。 |
第四章:配置与自定义
4.1 配置文件(.npmrc 与 pnpmfile.js)
| 配置文件 | 说明 | 注意事项 |
|---|
| .npmrc | pnpm 支持标准 npm 的 .npmrc 配置格式,用于设置 registry、store 路径、代理、认证等。可位于项目根目录、用户主目录或全局。 | pnpm 会合并多级 .npmrc(项目 > 用户 > 全局),优先级从高到低。 |
| pnpmfile.js | 用于自定义依赖解析逻辑,通过 hooks(如 readPackage)修改 package.json 内容,解决兼容性问题。 | 已在 pnpm v8+ 中弃用,推荐改用 pnpm.overrides 或 packageExtensions(见 4.4)。 |
| pnpm-workspace.yaml | 虽主要用于 workspaces,但也属于项目级配置文件,影响依赖链接行为。 | 必须放在 monorepo 根目录。 |
| 配置优先级 | 命令行参数 > 项目 .npmrc > 用户 .npmrc(~/.npmrc)> 环境变量(如 NPM_CONFIG_REGISTRY) | 环境变量需加前缀 NPM_CONFIG_(如 NPM_CONFIG_STORE_DIR=/tmp) |
4.2 设置 registry 与镜像源
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 设置默认 registry | 在 .npmrc 中写入 registry=https://registry.npmjs.org/ | 指定包下载源 | registry=https://registry.npmmirror.com/ | 国内常用镜像:npmmirror.com(原淘宝 NPM) |
| 为 scoped 包设置专用 registry | 在 .npmrc 中使用 @myorg:registry=https://... | 不同组织使用不同私有源 | @mycompany:registry=https://npm.mycompany.com/ | 支持多个 scoped registry |
| 临时使用镜像源 | 通过命令行参数 --registry | 单次命令指定源 | pnpm install --registry https://registry.npmmirror.com | 不影响持久配置 |
| 配置身份认证 | 在 .npmrc 中添加 _authToken=xxx 或 _auth=base64 | 访问私有 registry 需要 token | _authToken=NpmToken.xxxxxx | 敏感信息勿提交到 Git;建议使用环境变量注入 |
| 使用 .npmrc 模板 | 在 CI 中通过环境变量生成 .npmrc | 安全注入凭证 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc | 常见于 GitHub Actions、GitLab CI |
4.3 自定义 store 路径
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 通过 .npmrc 设置 | 在 .npmrc 中写入 store-dir=/custom/path | 指定全局 store 存储位置 | store-dir=/mnt/ssd/pnpm-store | 路径需有读写权限;支持相对路径(相对于 .npmrc 所在目录) |
| 通过环境变量设置 | 设置 PNPM_HOME 或 NPM_CONFIG_STORE_DIR | 动态控制 store 位置 | export PNPM_HOME=/opt/pnpm 后执行 pnpm install | PNPM_HOME 是 pnpm 专属;NPM_CONFIG_STORE_DIR 兼容 npm 风格 |
| 查看当前 store 路径 | pnpm store path | 输出当前生效的 store 目录 | pnpm store path | 用于调试或确认配置是否生效 |
| 多用户共享 store | 将 store-dir 设为公共目录(如 /shared/pnpm-store)并设置适当权限 | 团队机器节省磁盘空间 | 需确保所有用户对目录有读写权限 | 文件系统需支持硬链接(如 ext4、NTFS),不适用于 FAT32 |
4.4 钩子脚本(pnpmfile.js 中的 hooks)
⚠️ 注意:pnpmfile.js 自 pnpm v8 起已弃用,官方推荐使用 packageExtensions 和 overrides(在 package.json 或 .npmrc 中配置)。以下内容适用于旧版本或过渡场景。
| 钩子名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| readPackage | module.exports = { hooks: { readPackage } } | 在依赖安装前修改其 package.json 内容 | function readPackage(pkg) { if (pkg.name === 'broken-lib') { pkg.dependencies = ... } return pkg; } | 必须返回修改后的 pkg 对象;context 参数提供当前 lockfile 等信息 |
| afterAllResolved | hooks: { afterAllResolved(lockfile, context) { ... } } | 在 lockfile 生成后修改其内容 | 较少使用;可用于自定义 lockfile 后处理 | 返回值必须是合法 lockfile 对象 |
| 替代方案:packageExtensions | 在 package.json 中添加 pnpm.packageExtensions 字段 | 声明式修复依赖问题(推荐) | "pnpm": { "packageExtensions": { "broken-lib@*": { "dependencies": { "lodash": "^4.0.0" } } } } | 无需 JavaScript 文件;更安全、可静态分析 |
| 替代方案:overrides | 在 package.json 中使用 pnpm.overrides | 强制覆盖依赖版本(类似 yarn resolutions) | "pnpm": { "overrides": { "lodash": "^4.17.21", "**/react": "18.2.0" } } | **/ 表示任意深度;慎用,可能破坏兼容性 |
✅ 最佳实践:新项目应避免使用 pnpmfile.js,优先采用 packageExtensions 和 overrides 进行依赖修正。
第五章:工程实践
5.1 在 CI/CD 中使用 pnpm
| 操作步骤名称 | 操作细节 | 注意事项 |
|---|
| 安装 pnpm(推荐 Corepack) | 在 CI 脚本中启用 Corepack:corepack enable 后 corepack prepare pnpm@latest --activate | Node.js ≥ v16.13;避免使用 npm install -g pnpm 以防权限或缓存问题 |
| 缓存 pnpm store | 配置 CI 缓存目录为 ~/.pnpm-store(Linux/macOS)或 %LOCALAPPDATA%/pnpm/store(Windows) | 缓存 key 建议包含 pnpm-lock.yaml 的哈希值,如 ${{ hashFiles('**/pnpm-lock.yaml') }} |
| 安装依赖(冻结 lockfile) | 执行 pnpm install --frozen-lockfile | 确保 CI 环境与开发环境依赖完全一致;若 lockfile 过期则构建失败 |
| 并行测试(可选) | 使用 pnpm -r run test 在 monorepo 中并行运行所有子项目测试 | 可配合 --filter 限定范围,如 pnpm -r --filter="./packages/**" test |
| 构建产物 | 执行项目定义的构建脚本,如 pnpm run build | 确保 node_modules 已正确安装,且无幽灵依赖导致构建失败 |
| 使用 pnpm 官方 GitHub Action | 使用 pnpm/action-setup@v2 自动安装并缓存 | 示例:- uses: pnpm/action-setup@v2 with: version: 8 run_install: true |
5.2 与 TypeScript / Vite / Next.js 等框架集成
| 框架/工具 | 集成方式 | 代码示例 | 注意事项 |
|---|
| TypeScript | 正常安装 typescript 和 @types/*,pnpm 的非扁平化结构不影响类型解析 | pnpm add -D typescript @types/node 后 npx tsc --init | 确保 tsconfig.json 中 moduleResolution 为 node(默认),TypeScript 能正确解析符号链接 |
| Vite | 直接使用 create-vite 或手动初始化,pnpm 完全兼容 | pnpm create vite my-app --template react-ts 后 cd my-app && pnpm install | Vite 内部使用 esbuild 和原生 ESM,对 node_modules 结构无特殊要求 |
| Next.js | 使用 create-next-app 并指定包管理器为 pnpm | pnpm create next-app@latest --use-pnpm | Next.js ≥ v12 原生支持 pnpm;旧版本需设置 experimental: { transpilePackages: [...] } |
| React / Vue | 无特殊配置,直接安装即可 | pnpm add react react-dom | 若使用第三方 UI 库(如 antd、element-plus),确保其 peerDependencies 已显式安装 |
| ESLint / Prettier | 作为 devDependencies 安装,正常运行 | pnpm add -D eslint prettier 后 pnpm exec eslint src/ | pnpm 的 bin 链接机制确保 pnpm exec 或 pnpm run 能正确调用 CLI |
| Webpack | 无需额外配置;若使用 resolve.alias,注意不要硬编码 node_modules 路径 | resolve: { alias: { '@': path.resolve(__dirname, 'src') } } | 避免使用 new webpack.NormalModuleReplacementPlugin() 替换未声明依赖 |
5.3 多包项目(Monorepo)搭建示例
| 操作步骤名称 | 操作细节 | 注意事项 |
|---|
| 初始化根项目 | 创建项目根目录,执行 pnpm init -y,删除生成的 name 字段(monorepo 根通常不发布) | 根 package.json 可保留 scripts 用于统一命令 |
| 创建 pnpm-workspace.yaml | 在根目录创建文件:packages: - 'packages/*' - 'apps/*' | 路径支持 glob 模式;子目录需含有效 package.json |
| 创建子包 | 在 packages/utils 下执行 pnpm init,设置 "name": "@myorg/utils" | 建议使用 scoped name(如 @scope/name)便于管理 |
| 创建应用 | 在 apps/web 下初始化 React/Vite 项目,并添加对 @myorg/utils 的依赖 | 执行 pnpm add @myorg/utils,pnpm 自动创建符号链接 |
| 安装全部依赖 | 在根目录运行 pnpm install | pnpm 自动识别 workspace,本地包优先于 registry |
| 统一运行脚本 | 在根 package.json 添加:"scripts": { "build": "pnpm -r build" } | -r 表示递归执行所有子项目的同名脚本 |
| 发布子包 | 在根目录运行 pnpm -r publish --access public(首次需登录 npm) | 可配合 --filter 仅发布变更包,如 pnpm -r --filter "...[origin/main]" publish |
5.4 权限与安全策略(audit、lockfile)
| 功能名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 依赖审计(audit) | pnpm audit | 检查已安装依赖是否存在已知安全漏洞 | pnpm audit 或 pnpm audit --audit-level high | 数据源为 npm 安全公告;结果可能包含间接依赖漏洞 |
| 生成 lockfile | pnpm install(首次) | 创建 pnpm-lock.yaml,锁定依赖版本与完整性校验 | 自动生成 | 必须提交到 Git,确保团队和 CI 环境一致性 |
| 冻结 lockfile 安装 | pnpm install --frozen-lockfile | 若 lockfile 与 package.json 不匹配则报错 | pnpm install --frozen-lockfile | 强烈建议在 CI 中使用 |
| 忽略脚本(防恶意代码) | pnpm install --ignore-scripts | 跳过 preinstall/postinstall 等生命周期脚本 | pnpm install --ignore-scripts | 提高安全性,尤其在不可信依赖场景 |
| 设置私有 registry 权限 | 在 .npmrc 中配置 _authToken | 访问企业私有 npm 源 | //npm.mycompany.com/:_authToken=${NPM_TOKEN} | 敏感 token 应通过 CI 环境变量注入,勿硬编码 |
| 启用签名验证(实验性) | 尚未原生支持,需结合外部工具 | 验证包来源完整性 | 暂无内置方案 | 可考虑使用 Sigstore 或内部 CI 签名流程 |
第六章:性能与最佳实践
6.1 为什么 pnpm 更快更省空间
| 原理/机制 | 说明 | 注意事项 |
|---|
| 全局内容可寻址 Store | 所有包按内容哈希存储于全局 store(如 ~/.pnpm-store),相同版本/内容的包仅存一份物理文件。 | 多个项目共享依赖,磁盘占用减少 50%~90%,尤其在 monorepo 或大量项目场景。 |
| 硬链接复用 | 项目 node_modules 中的文件通过硬链接指向 store 中的真实文件,不复制数据。 | 硬链接不增加磁盘空间;但要求 store 与项目在同一文件系统(跨挂载点会回退为复制)。 |
| 并发下载与安装 | pnpm 支持并行下载包元数据和 tarball,并利用流式解压提升 I/O 效率。 | 首次安装速度接近 Yarn,后续安装因复用 store 极快(常 <1 秒)。 |
| 非扁平化结构避免重复解析 | 无需像 npm 那样进行复杂的依赖提升(dedupe)和冲突解决,安装逻辑更简单高效。 | 安装时间复杂度更低,尤其在大型依赖树中优势明显。 |
| 符号链接构建虚拟树 | 通过 .pnpm 目录中的符号链接组合出符合 Node.js 模块解析规则的视图,无需复制或移动文件。 | 启动时模块解析略慢于扁平化(微秒级差异),但安装和磁盘收益远大于此。 |
6.2 lockfile 的作用与版本锁定
| 概念名称 | 说明 | 注意事项 |
|---|
| pnpm-lock.yaml | 记录项目所有依赖(包括嵌套依赖)的确切版本、完整性校验(integrity hash)和解析结果。 | 必须提交到版本控制,确保所有开发者和 CI 环境使用完全一致的依赖。 |
| 版本锁定 | 即使 package.json 使用 ^1.0.0,lockfile 也会锁定到具体版本(如 1.2.3),防止”依赖漂移”。 | 避免”在我机器上能跑”的问题,提升构建可重现性。 |
| 完整性校验(Integrity) | 每个包条目包含 integrity 字段(如 sha512-…),安装时验证 tarball 内容是否被篡改。 | 提供供应链安全基础;若校验失败,pnpm 报错并拒绝安装。 |
| lockfile 更新时机 | 执行 pnpm add/remove/update 时自动更新;手动修改 package.json 后需运行 pnpm install 同步。 | 不要手动编辑 pnpm-lock.yaml,易出错且格式复杂。 |
| 与 npm/yarn lockfile 对比 | pnpm 的 lockfile 包含完整依赖图和符号链接结构信息,比 package-lock.json 更精确。 | 不可与其他包管理器混用(如 pnpm install + npm ci 会导致不一致)。 |
6.3 依赖解析规则与幽灵依赖防范
| 概念/规则 | 说明 | 注意事项 |
|---|
| 严格依赖隔离 | 每个包只能访问其 package.json 中显式声明的直接依赖(dependencies/devDependencies)。 | 若代码引用了未声明的包(如通过顶层 node_modules 访问),运行时将报 Cannot find module。 |
| 幽灵依赖(Phantom Dependency) | 指未在 package.json 中声明,却因扁平化 node_modules 而可被引用的包。 | npm/Yarn Classic 容易产生此问题;pnpm 默认杜绝,提升项目健壮性。 |
| PeerDependencies 处理 | pnpm 不自动安装 peerDependencies,需用户显式声明或由父包提供。 | 若插件要求 peerDependency(如 React),宿主项目必须安装对应版本。 |
| 模块解析路径 | Node.js 在解析 require('lodash') 时,会沿调用者路径向上查找 node_modules,pnpm 的符号链接结构确保此过程正常工作。 | 极少数工具(如某些 webpack loader)若硬编码遍历 node_modules 可能失效,需适配。 |
| 强制允许幽灵依赖(不推荐) | 通过设置 .npmrc 中 hoist=true 或 shamefully-hoist=true 模拟扁平化结构。 | 仅用于临时兼容旧项目;破坏 pnpm 核心优势,应避免长期使用。 |
6.4 推荐的团队协作规范
| 规范项 | 推荐做法 | 注意事项 |
|---|
| 统一包管理器 | 团队强制使用 pnpm,禁止混合使用 npm/yarn。 | 在项目根添加 .nvmrc + .tool-versions 或文档明确说明。 |
| 提交 lockfile | pnpm-lock.yaml 必须纳入 Git 版本控制。 | .gitignore 中不得忽略该文件。 |
| 禁用 shamefully-hoist | 不在 .npmrc 中启用 shamefully-hoist=true,除非临时兼容。 | 长期使用会引入幽灵依赖风险,违背 pnpm 设计初衷。 |
| 使用 Corepack 管理版本 | 通过 corepack enable + packageManager 字段(Node.js ≥ v16.13)固定 pnpm 版本。 | 在 package.json 中添加:"packageManager": "pnpm@8.15.0" |
| CI 中冻结安装 | CI 脚本使用 pnpm install --frozen-lockfile。 | 确保 CI 环境与本地开发完全一致,防止意外升级。 |
| Monorepo 目录规范 | 子包按功能分类,如 packages/(库)、apps/(应用)、tools/(工具)。 | 配合 pnpm-workspace.yaml 明确范围,避免误包含。 |
| 依赖声明原则 | 所有运行时引用的包必须显式写入 dependencies 或 devDependencies。 | 禁止依赖”碰巧存在”的包;可通过 pnpm dlx check-dependencies 等工具检测。 |
| 定期审计安全漏洞 | 在 CI 或定期任务中运行 pnpm audit --audit-level high。 | 结合 Dependabot 或 Renovate 自动创建升级 PR。 |