pnpm workspace 进阶技巧:过滤执行、依赖治理、缓存与 CI 稳定性

HTMLPAGE 团队
16 分钟阅读

深入讲解 pnpm workspace 在 monorepo 中的关键能力:workspace 协议、--filter 精准执行、依赖版本治理(overrides)、缓存加速、常见坑(幽灵依赖/peer 依赖/lockfile 冲突)与 CI 最佳实践。

#pnpm #workspace #monorepo #依赖管理 #CI #工程化

pnpm workspace 进阶技巧:过滤执行、依赖治理、缓存与 CI 稳定性

pnpm workspace 很容易“会用”,但要用到真正省时间、省成本,通常取决于三件事:

  1. 你能不能让命令只跑需要的部分(过滤执行)
  2. 你能不能把依赖问题从“玄学”变成可治理(版本策略、overrides、peer)
  3. 你能不能把 CI 做到快且稳定(缓存、锁文件策略、可重复构建)

本文按“你真实会遇到的问题”来组织内容,读完你应该能把 pnpm 用成团队的生产力工具。


1. workspace 的正确打开方式:显式、可追踪、可复用

1.1 pnpm-workspace.yaml

最基础但也最关键:

packages:
  - "packages/*"
  - "apps/*"

建议:

  • 不要把不相关的目录(例如历史模板、巨型数据目录)纳入 workspace
  • 目录边界越清晰,安装与扫描越快

1.2 workspace: 协议

在 monorepo 内部依赖,推荐:

{
  "dependencies": {
    "@acme/shared": "workspace:*"
  }
}

它的意义是:让“内部依赖”变得显式,并且跟随 workspace 的版本/链接策略。


2. 过滤执行:把 monorepo 的命令从“全量”变成“增量”

2.1 --filter:你最该掌握的能力

只对某个包执行:

  • pnpm --filter landing dev
  • pnpm --filter hola lint

只对某一类包执行(按包名 pattern):

  • pnpm --filter "@acme/*" build

只对某个目录下的包执行(当你包名不统一时很有用):

  • pnpm --filter "./packages/*" lint

2.2 递归执行:-r / --recursive

全仓统一跑:

  • pnpm -r lint
  • pnpm -r typecheck

注意:全仓跑适合做“基线”,但不适合做“常态”。


3. 依赖治理:把版本冲突从“踩雷”变成“可控”

3.1 pnpm.overrides:仓库级版本强制

当你需要:

  • 修复某个依赖的安全漏洞
  • 统一多包重复依赖的版本

可以在根 package.json 写:

{
  "pnpm": {
    "overrides": {
      "minimist": "1.2.8"
    }
  }
}

建议:

  • overrides 要有原因(最好写在 PR 描述/变更记录里)
  • 不要过度使用(否则升级会变困难)

3.2 peerDependencies:别把它当成“麻烦”,它是“边界声明”

典型场景:

  • UI 组件库通常对 vue/react 用 peerDependencies
  • 这样可以避免同一个应用里出现多份框架实例

当你看到 peer 相关告警时,不要第一反应“关掉告警”,而是:

  • 确认 peer 是否该由 consumer 提供
  • 确认版本范围是否合理

4. 缓存与安装速度:让 CI 变快的关键细节

4.1 pnpm store 与缓存

pnpm 的性能优势之一是内容寻址的 store 机制。

在 CI 中建议做两件事:

  • 使用 Node setup 的缓存(cache: pnpm)
  • 使用 pnpm install --frozen-lockfile 确保可重复

示意(GitHub Actions):

- uses: pnpm/action-setup@v4
  with:
    version: 9
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm
- run: pnpm install --frozen-lockfile

4.2 为什么要 --frozen-lockfile

它能保证:

  • 依赖解析不漂移
  • 你不会在 CI 里“悄悄更新 lockfile”

如果 CI 失败,说明:

  • lockfile 没提交
  • 或本地与 CI 的安装策略不一致

5. 常见坑与排障:pnpm 用久了必遇到

5.1 幽灵依赖(phantom deps)

现象:

  • 本地能跑,CI 报 Cannot find module

原因:

  • 某个包偷用别的包依赖,没写入自己的 package.json

解决思路:

  • 严格化 import(lint + 规则)
  • 减少 hoist(不要把一切都提升到根)
  • 让 CI 在干净环境全量安装并跑检查

5.2 lockfile 冲突

现象:多人同时改依赖后,pnpm-lock.yaml 合并冲突频繁。

建议:

  • 尽量把依赖升级集中到少数 PR
  • 合并冲突时优先使用 pnpm 的合并策略(必要时重新安装生成)
  • 避免手工编辑 lockfile

5.3 macOS EMFILE: too many open files

现象:开发时 watch 报文件句柄过多。

思路:

  • 提高 ulimit -n
  • 减少 watcher 范围(隔离大目录、排除 build 输出)

6. 把 pnpm 用成“工程化能力”:推荐的脚本设计

一个实用建议:把脚本按“检查/修复/构建”拆开,并统一命名。

示例:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "typecheck": "tsc -p tsconfig.json --noEmit",
    "build": "nuxt build"
  }
}

根目录再用 -r--filter 编排:

  • pnpm -r lint
  • pnpm --filter landing build

结语:pnpm 的“进阶”= 可执行的协作效率

当你把 pnpm 的过滤执行、版本治理与 CI 缓存组合起来,你会获得一个很现实的收益:

  • PR 检查更快
  • 失败更可定位
  • 团队协作更稳定

如果你希望我继续推进“写作计划未完成文章”,下一篇我建议优先补:

  • 代码质量门禁与 CI 集成(把 lint/typecheck/test 变成流程)
  • monorepo 工程化完整方案(边界 + 工具链 + CI)
  • CLS 问题排查与修复大全(指标到修复手段)