面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 23:47

pnpm 在 CI/CD 中如何加速安装和构建?

pnpm 在 CI/CD 里提速,核心是三件事:固定依赖、缓存 store、只构建必要包。pnpm install --frozen-lockfile 保证流水线不重新解析依赖;缓存 pnpm store 可以避免每次从网络下载;Monorepo 里用 --filter 只跑受影响的包,比全量构建更省时间。追问为什么优先缓存 pnpm store,而不是只缓存 node_modules?pnpm 的依赖真实内容放在 store,项目里的 node_modules 主要是链接。缓存 store 命中率更稳定,也更适合不同 job 复用。node_modules 可以缓存,但跨系统、跨 Node 版本时更容易出问题。GitHub Actions 里怎么配?用 actions/setup-node 的 cache: pnpm,再配合 pnpm/action-setup。如果要手动缓存,key 必须包含 pnpm-lock.yaml 的 hash,避免依赖变了还复用旧缓存。Docker 构建怎么提速?先复制 package.json 和 pnpm-lock.yaml,安装依赖后再复制源码。这样源码改动不会让依赖层失效。BuildKit 环境还可以挂载 pnpm store 缓存。Monorepo 怎么避免全量构建?用 pnpm -r --filter "...[origin/main]" build 只构建受影响包;需要并行时加 --workspace-concurrency,不要盲目开满 CPU,容易把 CI 机器打爆。写段代码- uses: pnpm/action-setup@v4 with: version: 9- uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm- run: pnpm install --frozen-lockfile --prefer-offline- run: pnpm -r --filter "...[origin/main]" build
服务端阅读 05月28日 02:26

pnpm 的 shamefully-hoist 配置是什么?什么时候需要使用?

shamefully-hoist 是 pnpm 提供的一个配置项,设为 true 后会将所有依赖提升到 node_modules 根目录,模仿 npm/Yarn 的扁平化结构。核心回答是什么: pnpm 默认使用内容寻址存储 + symlink 的严格依赖结构,每个包只能访问自己 package.json 中声明的依赖。shamefully-hoist=true 会打破这一限制,把全部包提升到根 node_modules,允许访问未声明的依赖(即幽灵依赖)。什么时候用: 只在两种场景下考虑使用——遗留项目迁移时临时启用,或者某些存在缺陷的工具(部分 webpack 插件、IDE 插件等)强制要求扁平化结构时。启用后应尽快迁移到 public-hoist-pattern 精细控制。默认结构 vs shamefully-hoistpnpm 默认的 node_modules 结构通过 .pnpm 目录和 symlink 实现严格隔离:# 默认结构(严格)node_modules/├── .pnpm/│ ├── lodash@4.17.21/│ │ └── node_modules/│ │ └── lodash/ # 实际文件│ └── express@4.18.2/│ └── node_modules/│ ├── express/│ └── debug/ # express 的依赖,对 lodash 不可见└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash# require 行为const lodash = require('lodash'); # OK - 声明了const debug = require('debug'); # 报错 - 未声明,访问不到启用 shamefully-hoist=true 后:# 扁平化结构node_modules/├── .pnpm/├── lodash/├── debug/ # 被提升上来├── express/└── ...# require 行为const lodash = require('lodash'); # OKconst debug = require('debug'); # 也能访问了(幽灵依赖)配置方式从 pnpm v8 开始,shamefully-hoist 推荐配置在 pnpm-workspace.yaml 而非 .npmrc(auth 和 registry 之外的设置都应如此):# pnpm-workspace.yamlshamefullyHoist: true旧版本仍可在 .npmrc 中配置:# .npmrcshamefully-hoist=true也可以通过命令行临时启用:pnpm install --shamefully-hoist更好的替代方案:public-hoist-patternshamefully-hoist=true 等价于 public-hoist-pattern=*,属于"一刀切"方案。绝大多数情况下,只需要提升特定包就够了:# .npmrc 或 pnpm-workspace.yamlpublic-hoist-pattern[]=*eslint*public-hoist-pattern[]=*prettier*public-hoist-pattern[]=*types*public-hoist-pattern[]=*webpack*三者的区别:| 配置 | 作用范围 | 提升位置 | 推荐度 ||------|----------|----------|--------|| shamefully-hoist | 所有包 | 根 nodemodules | 低(过渡用) || public-hoist-pattern | 匹配的包 | 根 nodemodules | 高 || hoist-pattern | 匹配的包 | .pnpm/node_modules | 中(内部可见) |public-hoist-pattern 提升到根目录,应用代码和工具都能访问;hoist-pattern 提升到 .pnpm/node_modules,只有其他依赖包能访问,应用代码看不到。实际迁移步骤从 npm/Yarn 迁移到 pnpm 时,推荐分步走:第一步:临时启用 shamefully-hoist# pnpm-workspace.yamlshamefullyHoist: true确保项目能正常运行:pnpm installpnpm buildpnpm test第二步:定位幽灵依赖# depcheck 可以检测未声明但使用的依赖npx depcheck# pnpm 自带命令查看依赖树pnpm ls --depth=0第三步:逐个修复将 depcheck 报出的缺失依赖添加到 package.json:pnpm add missing-dep第四步:切换到精细控制# pnpm-workspace.yamlshamefullyHoist: falsepublicHoistPattern: - "*eslint*" - "*prettier*" - "*types*"strictPeerDependencies: true再次验证构建和测试通过。不推荐长期使用的原因幽灵依赖隐患: 扁平化后代码可以 require 任何包,即使 package.json 没声明。这在 CI/CD 环境或版本升级时容易出问题——某个间接依赖升级或移除,你的代码就直接报错。版本冲突风险: 多个包依赖同一个包的不同版本时,扁平化只能保留一个版本,可能引发运行时错误。pnpm 的严格模式通过独立存储天然解决了这个问题。丧失 pnpm 的核心优势: 内容寻址存储的硬链接机制能节省大量磁盘空间(多项目共享同一份包文件),扁平化后这部分优势被削弱。相关追问node-linker 是什么? pnpm 还提供了 node-linker 配置,可以切换 node_modules 的整体布局方式:isolated(默认,symlink)、hoisted(类 npm 扁平化)、pnp(Yarn PnP 风格)。设置 node-linker=hoisted 是另一种实现扁平化的方式,效果和 shamefully-hoist=true 类似但不完全相同——它改变了整个布局策略而非仅做提升。pnpm v9 的变化? pnpm v9 进一步收紧了默认的 public-hoist-pattern,默认不再提升 eslint/types 等包。如果升级后构建报错,检查是否需要显式配置 public-hoist-pattern。
服务端阅读 05月28日 02:14

pnpm 如何处理 peer dependencies?与 npm 有什么不同?

pnpm 通过隔离的 node_modules 结构和严格的依赖图解析,在安装阶段就检测 peer dependencies 冲突,而 npm 的扁平化结构可能让版本不匹配的 peer 依赖静默通过,直到运行时才暴露问题。核心机制差异npm 采用扁平化 node_modules,依赖会被提升(hoist),包能访问到不该访问的依赖(幽灵依赖)。peer dependencies 版本不匹配时,npm v7 之前直接忽略,v7+ 会自动安装但可能产生重复实例。pnpm 使用 .pnpm 存储目录 + 符号链接的隔离结构,每个包只能访问自己声明的依赖。peer dependencies 从依赖图更高层级解析——如果宿主项目提供了匹配版本,符号链接指向它;否则安装报错:ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependenciesreact-dom@18.0.0 requires react@^18.0.0 but you have react@17.0.0这种严格性确保版本一致性,避免运行时因 React 多实例导致的 hooks 报错等问题。实际处理方式1. 匹配版本安装pnpm add react@18 react-dom@182. 全局覆盖{ "pnpm": { "overrides": { "react": "^18.0.0" } }}3. 自动安装 peer 依赖(.npmrc)auto-install-peers=truestrict-peer-dependencies=falseauto-install-peers=true 让 pnpm 自动解析并安装缺失的 peer 依赖,但不会解决版本冲突。strict-peer-dependencies=false 将冲突从报错降级为警告。4. 标记可选 peer 依赖{ "peerDependenciesMeta": { "react-dom": { "optional": true } }}monorepo 中的行为pnpm workspace 内,子包的 peer dependencies 会从同一 workspace 中其他包的 dependencies 中解析。workspace 依赖(workspace:*)的 peer 需求会被自动满足,无需额外配置。对比总结| 维度 | npm | pnpm ||------|-----|------|| 检查时机 | v7+ 安装时检查,v6 及以前延迟到运行时 | 始终在安装时严格检查 || 版本冲突 | 可能安装多个实例 | 单一实例,冲突直接报错 || node_modules 结构 | 扁平化,存在幽灵依赖 | 隔离存储 + 符号链接 || peer 解析来源 | 自行安装或从提升结构中查找 | 从依赖图高层级解析 |追问方向pnpm 的 .pnpm 目录结构如何保证 peer 依赖只链接到正确版本?auto-install-peers=true 在大型 monorepo 中可能带来什么问题?为什么 React 生态对 peer dependencies 尤其敏感?
服务端阅读 05月28日 02:12

什么是 pnpm,它与 npm 和 Yarn 有什么区别?

pnpm 是什么pnpm(Performant npm)是 Node.js 的包管理工具,核心设计目标是通过内容寻址存储解决 npm/Yarn 的磁盘浪费和幽灵依赖问题。存储机制:内容寻址 vs 扁平复制npm 和 Yarn classic 采用扁平化安装:每个项目的 node_modules 都复制一份完整的包文件。10 个项目用 lodash,磁盘上就有 10 份副本。pnpm 的做法不同——所有包只存一份到全局 store(通常在 ~/.local/share/pnpm/store),项目中通过硬链接指向 store 中的文件:# 查看全局 store 路径pnpm store path# /home/user/.local/share/pnpm/store/v3# 查看 store 中已缓存的包pnpm store status安装同一个包 10 次,磁盘上只有 1 份数据,项目中的 node_modules 只是硬链接指针。这就是 pnpm 能节省 70%+ 磁盘空间的原因。node_modules 结构:严格隔离 vs 幽灵依赖npm/Yarn 的扁平化结构会产生幽灵依赖——你可以在代码中 import 一个没有写在 package.json 里的包,因为它被其他依赖提升(hoisting)到了顶层。// 你的 package.json 只声明了 express// 但 express 依赖了 body-parser,body-parser 又依赖了 qs// npm 下这段代码能运行,因为 qs 被提升到了顶层const qs = require("qs") // 危险:没有在 package.json 中声明// 一天 express 换了依赖不再安装 qs,你的代码就崩了pnpm 的 node_modules 结构是这样的:node_modules/├── .pnpm/│ ├── express@4.18.2/│ │ └── node_modules/│ │ ├── express/ → 硬链接到 store│ │ └── body-parser/ → express 的依赖,只 express 能访问│ └── qs@6.11.0/│ └── node_modules/│ └── qs/ → 硬链接到 store└── express/ → 软链接到 .pnpm/express@4.18.2/node_modules/express每个包只能访问自己声明的依赖,require("qs") 如果没写在 package.json 里会直接报错。如果遇到必须用扁平结构的兼容性问题,可以在 .npmrc 中设置:shamefully-hoist=true但这会失去严格隔离的优势,只作为最后的兜底方案。安装速度对比冷安装(无缓存)和热安装(有缓存)的实际表现:| 场景 | npm | Yarn | pnpm ||------|-----|------|------|| 冷安装(1500 依赖) | ~48s | ~22s | ~14s || 热安装(已有缓存) | ~12s | ~3s | ~3s || 更新单个依赖 | ~9s | ~1.2s | ~0.9s |热安装快的原因:pnpm 检测到 store 中已有该包,直接创建硬链接,不需要网络请求和文件复制。monorepo 支持pnpm 原生支持 workspace,通过 pnpm-workspace.yaml 配置:# pnpm-workspace.yamlpackages: - "apps/*" - "packages/*"# 只安装某个 workspace 的依赖pnpm install --filter @myapp/web# 在所有 workspace 中执行脚本pnpm -r run build# 包间依赖引用pnpm add @myapp/utils --filter @myapp/web相比 npm workspaces,pnpm 的 --filter 语法更灵活,且 workspace 间的依赖也遵循严格隔离,不会意外访问兄弟包的内部模块。迁移到 pnpm# 从 npm 迁移:导入 lockfilepnpm import # 自动读取 package-lock.json 生成 pnpm-lock.yaml# 或直接安装rm -rf node_modules package-lock.jsonpnpm install# 从 Yarn 迁移同理,pnpm import 也支持 yarn.lock追问Q: pnpm 的硬链接在跨文件系统时会失效吗?会。硬链接要求源文件和目标在同一个文件系统分区。如果全局 store 和项目不在同一分区(比如 store 在 SSD,项目在机械硬盘),pnpm 会回退到复制文件,磁盘节省效果消失。Docker 容器中挂载卷时也需注意这个问题。Q: .npmrc 中 shamefully-hoist=true 和 hoist-pattern[] 怎么选?优先用 hoist-pattern 做精确控制,只提升特定包到顶层:# 只提升 eslint 相关包hoist-pattern[]=eslint*hoist-pattern[]=@eslint/*shamefully-hoist=true 是全部提升,等于放弃严格隔离,只在遇到无法绕过的兼容性问题时使用。
服务端阅读 05月28日 02:12

pnpm 常用命令有哪些?与 npm 命令有什么区别?

核心回答pnpm 常用命令与 npm 高度相似,关键差异在于:npm install <pkg> 对应 pnpm add <pkg>;npx 对应 pnpm dlx;monorepo 用 pnpm -r(递归)和 pnpm --filter(按包过滤)替代 npm workspaces。但命令相似只是表象,真正的区别在于底层机制——pnpm 使用内容寻址存储(content-addressable store)+ 符号链接结构,解决了 npm 的幽灵依赖和磁盘浪费问题。追问:为什么 pnpm 不存在幽灵依赖?npm 采用扁平化 nodemodules,所有依赖都被提升到顶层,导致代码能 import 未声明的包。pnpm 通过 .pnpm 目录存放硬链接,再用 symlink 严格映射到项目 nodemodules,只暴露 package.json 中声明的依赖。追问:pnpm add 和 npm install 的区别?npm install <pkg> 既用于安装所有依赖(无参数),也用于添加单个包。pnpm 将这两个职责拆分:pnpm install 安装已有依赖,pnpm add 添加新包,语义更清晰。安装与依赖管理# 安装所有依赖pnpm install# 添加依赖(默认 dependencies)pnpm add lodashpnpm add lodash@4.17.21# 添加到不同依赖组pnpm add lodash -D # devDependenciespnpm add lodash -O # optionalDependenciespnpm add lodash --save-peer # peerDependencies# 全局安装pnpm add -g typescript更新与删除# 更新pnpm update lodash # 更新单个包pnpm up -L # 更新所有包到最新版pnpm up -i # 交互式选择更新# 删除pnpm remove lodashpnpm rm lodash express # 删除多个包pnpm remove -g lodash # 删除全局包运行脚本与执行包# 运行 package.json 中的脚本pnpm build # 等同 pnpm run buildpnpm test# 传递参数pnpm build -- --watch# 执行远程包(替代 npx)pnpm dlx create-vite my-app追问:pnpm dlx 和 npx 有什么区别?pnpm dlx 下载包到临时目录执行,不污染全局。npx 在 npm 5.2+ 中引入,行为类似但会优先查找本地已安装的包。两者核心场景一致,但 pnpm dlx 的临时隔离更彻底。Monorepo 命令pnpm 原生支持 monorepo,通过 pnpm-workspace.yaml 声明工作区,配合 --filter 实现精准的包范围操作:# 在指定包中执行pnpm --filter @scope/pkg buildpnpm -F @scope/pkg build# 递归执行(所有包)pnpm -r build# 并行执行pnpm -r --parallel build# 只执行有变更的包pnpm -r --filter "...[origin/main]" build追问:pnpm workspace 和 npm workspaces 的核心差异?pnpm workspace 默认严格隔离依赖,通过 .npmrc 的 shamefully-hoist=true 可切换为扁平模式;npm workspaces 默认扁平提升。pnpm 的 --filter 支持正则、依赖图范围等灵活选择,npm 的 --workspace 功能相对简单。实际项目中 pnpm workspace 的依赖隔离更可靠,避免子包间隐式依赖。Store 管理pnpm 的核心优势来自全局 store,所有项目共享同一份包存储:pnpm store path # 查看 store 路径pnpm store prune # 清理未引用的包,释放磁盘pnpm store verify # 验证 store 完整性追问:pnpm store 的硬链接机制如何节省磁盘?所有项目共享一个全局 store(默认 ~/.local/share/pnpm/store),每个包只存一份。项目 node_modules 中通过硬链接指向 store,不复制文件。10 个项目用同一个版本的 lodash,磁盘只占一份空间。这也是 pnpm 安装速度比 npm 快 2-3 倍的原因——大部分包已在 store 中,只需创建链接。命令速查对比| 功能 | npm | pnpm | 差异说明 ||------|-----|------|----------|| 安装依赖 | npm install | pnpm install | 语义一致 || 添加包 | npm install <pkg> | pnpm add <pkg> | pnpm 语义更清晰 || 删除包 | npm uninstall <pkg> | pnpm remove <pkg> | 命令名不同 || 运行脚本 | npm run <cmd> | pnpm <cmd> | pnpm 可省略 run || 执行远程包 | npx <cmd> | pnpm dlx <cmd> | dlx 隔离更彻底 || 查看依赖 | npm list | pnpm list | 语义一致 || 查看过时包 | npm outdated | pnpm outdated | 语义一致 || 安全审计 | npm audit | pnpm audit | 语义一致 || 全局安装 | npm install -g | pnpm add -g | add vs install || Monorepo | --workspace | --filter | filter 更灵活 |其他实用命令pnpm init # 初始化 package.jsonpnpm create vite # 用模板创建项目pnpm import # 从 npm/yarn 锁文件迁移pnpm link ../local-pkg # 链接本地包pnpm why lodash # 查看包被谁依赖pnpm config list # 查看配置pnpm config set registry https://registry.npmmirror.com # 设置镜像源pnpm outdated # 检查过时依赖pnpm audit # 安全审计
服务端阅读 05月28日 02:10

pnpm workspace 如何配置和使用?

pnpm workspace 是 pnpm 内置的 monorepo 方案,让你在一个仓库里管理多个互相依赖的包。相比 yarn workspaces 和 lerna,它零额外依赖、硬链接共享磁盘空间,配置也最简单。如何声明 workspace在项目根目录创建 pnpm-workspace.yaml:packages: - 'packages/*' - 'apps/*' - 'shared/*'根目录的 package.json 必须设置 "private": true,防止根包被误发布。典型目录结构my-monorepo/├── pnpm-workspace.yaml├── package.json # private: true├── pnpm-lock.yaml├── .npmrc # pnpm 专用配置├── packages/│ ├── ui/│ │ ├── package.json # name: @my-org/ui│ │ └── src/│ └── utils/│ ├── package.json # name: @my-org/utils│ └── src/└── apps/ ├── web/ │ ├── package.json # name: @my-org/web │ └── src/ └── server/ ├── package.json └── src/包间依赖如何引用使用 workspace: 协议引用本地包,开发时直接链接源码,不用发布再安装:// apps/web/package.json{ "name": "@my-org/web", "dependencies": { "@my-org/ui": "workspace:*", "@my-org/utils": "workspace:^1.0.0" }}workspace: 后面的版本写法决定了发布时的替换规则:| 协议写法 | 开发时行为 | 发布时替换为 ||---------|-----------|-------------|| workspace:* | 链接最新 | 精确版本如 1.2.3 || workspace:^ | 链接最新 | ^1.2.3 || workspace:~ | 链接最新 | ~1.2.3 || workspace:^1.0.0 | 链接最新 | ^1.0.0(保留原范围) |发布时 workspace: 会被自动替换为实际版本号,这是 pnpm 的内置行为,不需要额外配置。常用命令速查# 安装所有包的依赖pnpm install# 在指定包中执行命令(--filter 或 -F)pnpm --filter @my-org/ui buildpnpm -F @my-org/ui build# 递归执行:所有包都跑 buildpnpm -r build# 只构建有变更的包(基于 git diff)pnpm -r --filter "...[origin/main]" build# 给指定包添加依赖pnpm --filter @my-org/web add @my-org/ui# 给根目录添加公共开发依赖(-w 标志)pnpm add -Dw eslint prettier-r(递归)和 --filter 的区别:-r 对所有包执行,--filter 按条件筛选。生产环境推荐用 --filter 精确控制,避免无关包被意外构建。.npmrc 关键配置pnpm workspace 的许多行为可以通过 .npmrc 调整:# 未在 workspace 找到的包是否从 registry 下载(默认 true)link-workspace-packages=true# 依赖提升策略(避免幽灵依赖)shamefully-hoist=falsenode-linker=hoisted # 需要 hoist 时用这个,不推荐# 严格对等依赖strict-peer-dependencies=truelink-workspace-packages=true 配合 workspace:* 使用时,如果本地包版本满足范围就链接本地,否则从 registry 下载。这在逐步迁移 monorepo 时很有用。用 changesets 管理版本和发布多包版本管理推荐用 changesets,它是 pnpm 官方推荐的方案:# 安装pnpm add -Dw @changesets/clipnpm changeset init# 工作流pnpm changeset # 交互式记录本次变更(选包、选版本类型、写 changelog)pnpm changeset version # 根据记录更新 package.json 和 CHANGELOG.mdpnpm -r publish # 发布所有有新版本的包changeset init 会生成 .changeset/ 目录和 config.json,其中可以配置 changelog 格式、access(public/restricted)等。常见问题Q: 修改了子包代码,依赖它的包要重新安装吗?不需要。workspace:* 链接的是源码,修改即生效(前提是需要重新 build 的包要重新构建)。Q: 发布时 workspace: 协议怎么处理?pnpm publish 时 workspace:* 会自动替换为 package.json 中的实际版本号。如果版本号不存在会报错。Q: 怎么排查某个包的依赖关系?pnpm list --filter @my-org/web --depth 1 # 查看依赖树pnpm why @my-org/ui --filter @my-org/web # 查看为什么依赖这个包Q: 子包之间循环依赖怎么办?pnpm 会报错。解决方式是抽取共享逻辑到第三个包,或者通过接口/事件解耦。pnpm workspace vs 其他方案| 特性 | pnpm workspace | Yarn Workspaces | Lerna | Turborepo ||------|---------------|-----------------|-------|-----------|| 内置支持 | 是 | 是 | 需安装 | 需安装 || 依赖存储 | 硬链接(全局 store) | 符号链接 | 各自安装 | 依赖 pnpm/yarn || 磁盘效率 | 最高 | 中等 | 最低 | 取决于底层 || 任务缓存 | 否 | 否 | 否 | 是 || 配置复杂度 | 低 | 低 | 高 | 中 || 版本管理 | 需配合 changesets | 需配合工具 | 内置 | 需配合工具 |pnpm workspace 本身只解决依赖管理和包链接,任务编排(缓存、并行)交给 Turborepo 或 Nx,版本发布交给 changesets——这是目前最主流的组合方案。
服务端阅读 05月28日 02:10

pnpm 的 overrides 和 resolutions 有什么区别?如何使用?

pnpm.overrides:原生覆盖机制pnpm.overrides 是 pnpm 原生的依赖覆盖配置,写在 package.json 的 pnpm 字段中,也可以写在 pnpm-workspace.yaml 里。// package.json{ "pnpm": { "overrides": { "lodash": "^4.17.21", "react": "^18.0.0" } }}# pnpm-workspace.yamloverrides: "lodash": "^4.17.21" "react": "^18.0.0"精确路径覆盖用 > 指定只覆盖某个包的依赖,不影响其他包对同一依赖的使用:{ "pnpm": { "overrides": { "webpack>lodash": "^4.17.21", "antd>rc-util": "^5.30.0" } }}也可以限定只覆盖特定版本的依赖:{ "pnpm": { "overrides": { "minimist@<1.2.6": "^1.2.6" } }}引用项目直接依赖用 $ 前缀引用项目自身声明的依赖版本,避免硬编码版本号导致不一致:{ "dependencies": { "react": "^18.2.0" }, "pnpm": { "overrides": { "react": "$react" } }}这样所有间接依赖的 react 都会使用项目声明的 ^18.2.0,而不是单独写一个版本。移除依赖用 - 可以把某个依赖从依赖树中移除:{ "pnpm": { "overrides": { "some-package>unused-dep": "-" } }}这在处理可选依赖或减少安装体积时有用。替换包将一个包替换为另一个:{ "pnpm": { "overrides": { "node-sass": "sass", "request": "axios" } }}对 peerDependencies 生效pnpm.overrides 会覆盖 peerDependencies 的版本解析。这在统一 React 版本时很常见——某些组件库的 peerDependency 声明了旧版 React,但你希望它们都使用项目中的版本。resolutions:Yarn 兼容字段resolutions 是 Yarn 的依赖覆盖字段,pnpm 为了方便从 Yarn 迁移的项目也支持读取它:{ "resolutions": { "lodash": "^4.17.21" }}resolutions 的局限resolutions 在 pnpm 中是兼容层,功能比 pnpm.overrides 少很多:不支持 > 路径指定,只能全局覆盖不支持 $ 引用直接依赖不支持 - 移除依赖不支持 @版本号 限定范围只能写在 package.json 中,不支持 pnpm-workspace.yaml两者优先级当 pnpm.overrides 和 resolutions 同时存在时,pnpm.overrides 优先级更高。如果同一条目两边都写了,以 pnpm.overrides 为准。有个已知问题:当 pnpm-workspace.yaml 中写了 overrides 时,package.json 里的 resolutions 可能被忽略而不是合并。所以迁移项目时建议把 resolutions 手动迁移到 pnpm.overrides,而不是两边混用。区别对比| 特性 | pnpm.overrides | resolutions ||------|----------------|-------------|| 路径指定(>) | 支持 | 不支持 || 版本限定(@版本) | 支持 | 不支持 || 引用直接依赖($) | 支持 | 不支持 || 移除依赖(-) | 支持 | 不支持 || 配置位置 | package.json 或 pnpm-workspace.yaml | 仅 package.json || peerDependencies 覆盖 | 支持 | 支持 || 优先级 | 高 | 低 || 来源 | pnpm 原生 | Yarn 兼容 |什么时候用哪个新项目直接用 pnpm.overrides,不需要考虑 resolutions。从 Yarn 迁移的项目,短期可以保留 resolutions 让项目先跑起来,但应尽快迁移到 pnpm.overrides,否则缺少路径指定和版本限定功能,覆盖精度不够,还可能遇到优先级冲突问题。常见使用场景修复传递依赖的安全漏洞第三方包依赖了有漏洞的旧版本,该包还没更新,你先手动覆盖:{ "pnpm": { "overrides": { "follow-redirects@<1.15.6": "1.15.6" } }}统一 React 版本monorepo 里不同包可能依赖不同版本的 React,用 $ 引用统一:{ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "pnpm": { "overrides": { "react": "$react", "react-dom": "$react-dom" } }}只覆盖某个包的依赖某个包依赖了不兼容的版本,但你不想影响其他包:{ "pnpm": { "overrides": { "antd>rc-util": "^5.30.0" } }}验证覆盖效果修改 overrides 后需要重新安装依赖:pnpm install用以下命令确认覆盖是否生效:# 查看实际安装的版本pnpm list react# 查看某个依赖被谁引入pnpm why lodash# 查看完整依赖树pnpm list --depth=10注意事项全局覆盖要谨慎。把所有包的某个依赖强制升到大版本,可能导致不兼容。优先用路径覆盖(>)只影响目标包。修改 overrides 后记得重新 pnpm install,如果锁文件没更新,可以 pnpm install --force 强制刷新。
服务端阅读 05月28日 01:05

pnpm 如何处理依赖版本冲突?

pnpm 如何处理依赖版本冲突?你刚用 pnpm 装完依赖,终端却飘红一片:ERRPNPMPEERDEPISSUE。或者更隐蔽——项目跑起来了,但某个库拿到的不是它期望的依赖版本,线上偶发一个幽灵 bug。这些都是依赖版本冲突的典型表现。pnpm 的严格隔离机制让冲突更容易暴露,但也给了你更精确的解决手段。这篇文章把 pnpm 处理版本冲突的机制和实战解法一次性讲透。冲突是怎么产生的?一个项目同时依赖 package-a 和 package-b,它们各自要求不同版本的 lodash:{ "dependencies": { "package-a": "^1.0.0", "package-b": "^2.0.0" }}// package-a 依赖 lodash@^4.17.0// package-b 依赖 lodash@^3.10.0npm/Yarn 把依赖扁平化到 node_modules 顶层,同一时刻只能存在一个版本的 lodash。谁先安装谁占位,另一个包可能拿到错误版本——这种行为是不确定的,换台机器结果可能就不一样。pnpm 的核心机制:隔离存储 + 精确链接pnpm 不做扁平化。它在 node_modules/.pnpm 下为每个包创建独立目录,各自持有完整的依赖树,再通过符号链接暴露给项目:node_modules/├── .pnpm/│ ├── lodash@3.10.1/│ │ └── node_modules/lodash│ ├── lodash@4.17.21/│ │ └── node_modules/lodash│ ├── package-a@1.0.0/│ │ └── node_modules/│ │ ├── package-a│ │ └── lodash -> ../../lodash@4.17.21/node_modules/lodash│ └── package-b@2.0.0/│ └── node_modules/│ ├── package-b│ └── lodash -> ../../lodash@3.10.1/node_modules/lodash├── package-a -> .pnpm/package-a@1.0.0/node_modules/package-a└── package-b -> .pnpm/package-b@2.0.0/node_modules/package-bpackage-a 引用 lodash 解析到 4.17.21,package-b 解析到 3.10.1,两者互不干扰。同时 .pnpm 下的包通过硬链接指向全局 store(默认 ~/.local/share/pnpm/store),同一版本在磁盘上只存一份。这意味着:大多数"版本冲突"在 pnpm 下其实不会造成问题——两个版本可以共存。 真正需要你介入的是 peer dependency 冲突和需要全局统一版本的场景。如何排查依赖冲突?遇到问题先定位,再动手:# 查看项目依赖树pnpm list# 追溯某个包被谁依赖pnpm why lodash# 查看完整深度依赖树pnpm list --depth=10# 检查重复依赖pnpm list --depth=Infinity | grep lodash强制统一版本:overrides当多个依赖要求同一包的不同版本,而你希望全局统一时,使用 pnpm.overrides:{ "pnpm": { "overrides": { "lodash": "^4.17.21" } }}只覆盖某个包的子依赖,用 > 精准定位:{ "pnpm": { "overrides": { "package-b>lodash": "^4.17.21" } }}用版本范围选择器,只重写匹配的版本:{ "pnpm": { "overrides": { "lodash@^3": "^4.17.21" } }}overrides 会覆盖整个依赖树的解析结果,用前确认这是你想要的。 如果只想解决某个子树的冲突,优先用 > 语法。peer dependencies 冲突处理peer dependency 冲突是 pnpm 用户最常遇到的报错。比如 package-a 要求 react>=16.8.0,package-b 要求 react>=17.0.0,pnpm 默认报 ERRPNPMPEERDEPISSUE。方案一:安装满足所有约束的版本(推荐)pnpm add react@18react@18 同时满足 >=16.8.0 和 >=17.0.0,冲突自然消除。这是最干净的解法。方案二:overrides 强制统一{ "pnpm": { "overrides": { "react": "^18.0.0" } }}方案三:peerDependencyRules 宽松处理在 .npmrc 中配置规则,允许特定版本范围或忽略缺失的 peer 依赖:# 允许特定版本的 peer 依赖peerDependencyRules.allowedVersions.react=>=16.8.0 <19# 忽略缺失的 peer 依赖peerDependencyRules.ignoreMissing=react-dom或开启自动安装 peer 依赖(pnpm v8+ 默认开启):auto-install-peers=true选择原则: 能装统一版本就装,不能装就用 overrides,只有当你明确知道忽略是安全的时候才用 peerDependencyRules。依赖去重pnpm 的 store 机制天然去重:同一版本在全局只存一份,各项目通过硬链接共享。但如果依赖树中存在多个可兼容版本(如 lodash@4.17.20 和 lodash@4.17.21),用 dedupe 合并:pnpm dedupe该命令将依赖树中可兼容的重复包合并为单一版本,减少冗余。合并后建议检查 lockfile 变更,确认无意外升级。monorepo 中的版本冲突在 monorepo 中,不同 workspace 可能依赖同一包的不同版本。除了 overrides,还有三种方案:方案一:catalog 协议统一版本(pnpm v9+,推荐)# pnpm-workspace.yamlcatalogs: default: react: ^18.2.0 lodash: ^4.17.21各 workspace 引用时:{ "dependencies": { "react": "catalog:", "lodash": "catalog:" }}catalog 是 pnpm 原生的版本管理方案,比 overrides 更语义化,改动也更容易追踪。方案二:共享 lockfilepnpm monorepo 默认共享一个 pnpm-lock.yaml,确保所有 workspace 的依赖解析一致。如果你手动拆分了 lockfile,建议改回共享模式。方案三:hoist-pattern 提升公共依赖# .npmrc — 将匹配的包提升到根 node_moduleshoist-pattern[]=*eslint*hoist-pattern[]=*prettier*提升会破坏 pnpm 的严格隔离,只在确有需要时使用。lockfile 合并冲突处理多人协作时 pnpm-lock.yaml 可能产生 git 合并冲突:# 合并后重新生成 lockfile(pnpm 会自动处理冲突)pnpm install# CI 环境严格校验 lockfile 一致性pnpm install --frozen-lockfilepnpm 内置了冲突修复算法(由 @pnpm/merge-lockfile-changes 维护),合并时以目标分支的版本为准。如果冲突复杂,删掉 lockfile 重新生成也是安全的——只是会丢失确定性,建议合并后让团队成员确认。npm/Yarn vs pnpm 冲突处理对比| 特性 | npm/Yarn | pnpm ||------|----------|------|| 多版本共存 | 扁平化冲突,可能拿到错误版本 | 独立存储,精确链接,天然共存 || 依赖隔离 | 无隔离,可访问未声明的依赖 | 严格隔离,只能访问声明的依赖 || 磁盘占用 | 每个项目独立安装 | 硬链接共享 store,多项目共用 || 版本统一 | npm overrides / yarn resolutions | overrides + peerDependencyRules || monorepo 版本管理 | workspaces | workspace + catalog 协议 || lockfile 冲突 | 手动解决 | 内置合并算法 |实战检查清单遇到 pnpm 依赖冲突,按这个顺序排查:pnpm why <package> — 搞清楚谁在要什么版本判断是否真有冲突 — pnpm 的隔离机制允许不同版本共存,多数场景不需要干预peer dependency 报错 — 优先安装满足所有约束的版本;不行就用 overrides需要全局统一版本 — 用 overrides,子树隔离用 > 语法monorepo 版本对齐 — 用 catalog 协议,比 overrides 更可维护lockfile 合并冲突 — 直接 pnpm install,pnpm 会自动处理CI 环境用 --frozen-lockfile — 确保构建可复现,意外冲突在 CI 阶段暴露
服务端阅读 05月28日 01:04

pnpm 的 node_modules 结构是怎样的?为什么这样设计?

核心答案pnpm 的 node_modules 采用符号链接 + 硬链接的混合结构,由三层组成:node_modules/[package] — 符号链接层,只暴露直接依赖nodemodules/.pnpm/[package@version]/nodemodules/ — 实际包内容,硬链接到全局 store全局 store(~/.local/share/pnpm-store) — 内容寻址存储,所有项目共享这样设计的核心目标是:消灭幽灵依赖 + 节省磁盘空间 + 保持 Node.js 模块解析兼容。目录结构详解node_modules/├── .pnpm/ # 实际内容存放区│ ├── lodash@4.17.21/│ │ └── node_modules/│ │ └── lodash/ # 硬链接 → 全局 store│ ├── express@4.18.2/│ │ └── node_modules/│ │ ├── express/ # 硬链接 → 全局 store│ │ ├── body-parser/ # express 的依赖(符号链接)│ │ └── ...│ └── .modules.yaml # pnpm 元数据├── lodash → .pnpm/lodash@4.17.21/node_modules/lodash # 符号链接└── express → .pnpm/express@4.18.2/node_modules/express # 符号链接关键点:你的代码 require('lodash') 时,Node.js 解析路径是 node_modules/lodash,它是一个符号链接,最终指向 .pnpm 中的真实包。而 .pnpm/lodash@4.17.21/node_modules/lodash/ 下的每个文件,都是硬链接到全局 store 的——不占额外磁盘空间。三层结构各自的职责符号链接层:严格隔离依赖node_modules/├── lodash → .pnpm/lodash@4.17.21/node_modules/lodash└── express → .pnpm/express@4.18.2/node_modules/express这一层只包含 package.json 中声明的直接依赖。你声明了什么,就只能访问什么。// package.json 只声明了 express// npm/yarn 下 body-parser 被 hoist 到顶层,可以直接访问const bodyParser = require('body-parser') // npm: ✅ 能用 pnpm: ❌ 找不到// 这就是"幽灵依赖"——你用了没声明的东西,某天它被移除就挂了.pnpm 层:依赖的真实组织.pnpm 目录按 包名@版本 平铺,每个包下有自己的 node_modules,包含该包的所有依赖(也是符号链接)。这样每个包只能看到自己声明的依赖,形成严格的依赖图:.pnpm/├── express@4.18.2/│ └── node_modules/│ ├── express/ # 包自身内容(硬链接)│ ├── body-parser/ # → .pnpm/body-parser@1.20.2/... (符号链接)│ └── cookie/ # → .pnpm/cookie@0.5.0/... (符号链接)└── body-parser@1.20.2/ └── node_modules/ ├── body-parser/ └── raw-body/ # → .pnpm/raw-body@2.5.1/...每个包的依赖都在自己的 node_modules 下,不会泄漏到外部。全局 store:内容寻址去重# 查看 store 位置pnpm store path# ~/.local/share/pnpm-store/v3# 同一台机器上 10 个项目都用 lodash@4.17.21# 磁盘上只存一份,每个项目的 node_modules 里的文件是硬链接ls -i node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/lodash.js# inode 号相同 → 同一份磁盘数据硬链接意味着:同一台机器上,无论多少个项目安装同一个包的同一个版本,磁盘上只有一份数据。删除某个项目不会影响 store 中的文件(硬链接引用计数 > 0 就不删除)。为什么这样设计:解决 npm/yarn 的三大痛点痛点一:幽灵依赖npm v3 之前用嵌套结构,依赖层级太深(Windows 路径 260 字符限制)。npm v3 改成扁平化,所有依赖被 hoist 到 node_modules 根目录:# npm 的扁平结构node_modules/├── express/├── body-parser/ ← 你没声明但能访问├── raw-body/ ← 你没声明但能访问├── cookie/ ← 你没声明但能访问└── ...pnpm 的符号链接层只暴露直接依赖,从结构上杜绝了幽灵依赖。痛点二:磁盘浪费npm/yarn 每个项目的 node_modules 都是完整拷贝。10 个项目用 React,磁盘上存 10 份。pnpm 通过硬链接共享全局 store,同一版本只存一份。实际效果:# 用 npm 安装一个新项目du -sh node_modules # 200MB# 用 pnpm 安装(已有其他项目装过相同依赖)du -sh node_modules # 目录大小仍显示 200MB(ls -l 看大小)# 但实际磁盘占用接近 0(硬链接不占额外空间)du -sh ~/.local/share/pnpm-store # store 总大小可能也才 200MB痛点三:依赖分身npm 的扁平化可能导致同一个包的多个版本都被 hoist,只有一个能到顶层,其他版本仍嵌套。项目可能同时依赖一个包的两个版本而不自知,产生难以排查的 bug。pnpm 的 .pnpm/name@version 结构天然支持多版本共存,每个版本独立存储、独立链接。peer dependencies 的处理pnpm 对 peer dependencies 的处理比 npm 更严格。如果包 A 声明了 peer dependency React,但项目中没有安装,pnpm 会报错而非静默跳过:ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies.└─┬ foo@1.0.0 └── ✕ missing peer react@>=16.0.0在 .pnpm 结构中,peer dependencies 会被正确地提升到依赖它的包可见的层级,而不是像 npm 那样依赖 hoisting 的不确定性。追问:硬链接和符号链接的区别是什么?硬链接:指向文件的 inode,和原文件共享同一份数据块。删除原文件不影响硬链接,反之亦然。pnpm 用它连接 store 和各项目的 node_modules,实现零拷贝共享。符号链接(软链接):指向文件路径,类似快捷方式。pnpm 用它构建依赖关系图:node_modules/lodash → .pnpm/lodash@4.17.21/node_modules/lodash,让 Node.js 的模块解析逻辑正常工作。两者配合:符号链接解决依赖可见性,硬链接解决磁盘空间。
服务端阅读 05月28日 01:02

pnpm 的性能优势体现在哪些方面?与 npm/Yarn 对比如何?

pnpm 的性能优势集中在三个层面:安装速度、磁盘占用和依赖安全性。下面逐项拆解,并与 npm、Yarn 做横向对比。安装速度:硬链接让缓存安装接近即时pnpm 在有缓存时的安装速度远超 npm 和 Yarn,核心原因是硬链接机制——同一台机器上只要某个包的版本曾经下载过,后续项目安装时直接从全局 store 创建硬链接,无需重复拷贝文件。| 场景 | npm | Yarn Berry (PnP) | pnpm ||------|-----|------------------|------|| 冷安装(无缓存) | 45-55s | 30-40s | 25-35s || 热安装(有缓存) | 18-25s | 10-15s | 2-4s || 删 node_modules 重装 | 40-50s | 25-35s | 3-5s |数据来源:基于 2026 年社区基准测试(Monorepo 场景,~1500 依赖)。冷安装差距主要来自 pnpm 的并行下载策略;热安装差距则完全由硬链接驱动——pnpm 的热安装本质上是创建文件链接,而非文件拷贝。磁盘空间:内容寻址存储节省 70% 以上npm 和 Yarn 的 node_modules 采用扁平化拷贝:每个项目独立存储所有依赖文件。10 个使用相同技术栈的项目,磁盘占用约为 500MB x 10 = 5GB。pnpm 使用全局内容寻址存储(content-addressable store):相同内容的文件只存一份,各项目通过硬链接引用。| 项目数量 | npm 总占用 | pnpm 总占用 | 节省比例 ||---------|-----------|------------|--------|| 1 个 | ~500MB | ~500MB | 0% || 5 个 | ~2.5GB | ~550MB | 78% || 10 个 | ~5GB | ~600MB | 88% |项目越多,节省越显著。这对 CI 环境和磁盘受限的开发机尤其重要。依赖安全性:严格模式杜绝幻影依赖这常常被忽略,但却是 pnpm 架构上最重要的区别。npm 和 Yarn 采用扁平化 hoisting——所有依赖(包括间接依赖)都被提升到 node_modules 根目录。这意味着你可以在代码中 require 一个未在 package.json 中声明的包,只要它恰好被其他依赖安装了。这就是幻影依赖(phantom dependency)。pnpm 的 node_modules 采用嵌套软链接结构:node_modules/ .pnpm/ lodash@4.17.21/ node_modules/ lodash -> 硬链接到全局 store lodash -> .pnpm/lodash@4.17.21/node_modules/lodash你只能访问 package.json 中显式声明的包。间接依赖被严格隔离在 .pnpm 目录内,不会泄露到你的代码中。这避免了以下常见问题:某个间接依赖被上游移除后,你的代码突然报错不同版本的同名包产生意外冲突团队成员之间依赖行为不一致架构差异决定性能差距三种包管理器的核心区别在于 node_modules 的组织方式:| 特性 | npm | Yarn Berry | pnpm ||------|-----|-----------|------|| node_modules 结构 | 扁平化拷贝 | PnP 虚拟文件系统 | 嵌套软链接 + 硬链接 || 依赖存储 | 每项目独立拷贝 | 单个 .pnp.cjs 映射 | 全局 store 共享 || 幻影依赖 | 有 | 无(PnP 模式) | 无 || 硬链接复用 | 不支持 | 不支持 | 支持 || Monorepo 支持 | 基础 workspaces | 原生支持 | 原生 + workspace 协议 |什么场景选 pnpm多项目开发环境:磁盘节省最明显,热安装接近即时Monorepo 项目:内置 workspace 协议和过滤,原生支持子项目间依赖联动CI/CD 流水线:配合 prefer-frozen-lockfile=true,冷安装速度也有明显优势团队协作:严格依赖模式保证每个人的依赖行为一致,减少"我这能跑你那不行"的问题什么场景不选 pnpm小型项目、原型验证:npm 零配置直接上手更省事依赖大量 native 模块(如 node-gyp):硬链接有时会遇到权限问题现有项目已深度依赖 Yarn 插件生态:迁移成本需要评估追问:pnpm 的局限性和注意事项严格模式可能导致兼容问题:某些包假设了扁平化结构,在 pnpm 下可能报错,可通过 .npmrc 中设置 shamefully-hoist=true 回退到扁平模式(但会失去幻影依赖保护)全局 store 需要定期清理:使用 pnpm store prune 清理不再被引用的包与 Yarn PnP 的取舍:Yarn PnP 甚至不生成 node_modules 目录,安装更快但生态兼容性更差;pnpm 在性能和兼容性之间取了更好的平衡