← 返回文章列表
前端工程化·957 次阅读

pnpm + Workspace:中小团队 monorepo 落地手记

事故回顾:运维后台和商户后台各 copy 了一份"共享"组件库,A 项目修了表格组件的分页 bug,B 项目没人同步,上周商户反馈数据错乱。CTO 拍板:合并 monorepo。

调研一天后选了 pnpm workspace(对比了 yarn workspaces 和 lerna),两周完成迁移。这篇是实战记录。

为什么是 pnpm

三个候选的对比:

  • npm workspaces:起步晚,功能基础够用但安装慢
  • yarn workspaces:经典但安装速度一般,node_modules 依然扁平化的坑
  • pnpm:硬链接 + 符号链接的方案,装得快、省磁盘、依赖隔离严格

让我下决心的是 pnpm 的"严格":默认不允许访问未声明的依赖(幽灵依赖),这个特性在迁移时帮我们揪出了 7 处隐式依赖的埋雷代码。

目标结构

apps/
  admin/          # 运维后台
  merchant/       # 商户后台
  h5/             # C 端 H5
packages/
  ui/             # 共享组件库
  utils/          # 共享工具函数
  config/         # 共享 eslint/tsconfig
pnpm-workspace.yaml
package.json

pnpm-workspace.yaml 声明工作区范围:

packages:
  - 'apps/*'
  - 'packages/*'

根 package.json 只放全局工具和脚本:

{
  "private": true,
  "scripts": {
    "dev:admin": "pnpm --filter admin dev",
    "build": "pnpm -r --filter './apps/*' build",
    "test": "pnpm -r test"
  },
  "devDependencies": {
    "typescript": "^4.8.0"
  }
}

包之间怎么引用

ui 包的 package.json 起好名字:

{
  "name": "@company/ui",
  "version": "1.0.0",
  "main": "src/index.ts"
}

应用里引用 workspace 内的包,用 workspace:* 协议:

{
  "name": "admin",
  "dependencies": {
    "@company/ui": "workspace:*",
    "@company/utils": "workspace:*"
  }
}

workspace:* 表示"永远用工作区里的本地版本",发布时 pnpm 会自动替换成真实版本号。本地开发改 ui 的代码,admin 里立即生效,不用发版不用 link——这就是 monorepo 的核心爽点。

高频命令速查

pnpm install                          # 根目录一次安装所有包的依赖
pnpm --filter admin dev               # 只跑 admin 的 dev
pnpm --filter @company/ui build       # 只构建 ui 包
pnpm -r build                         # 所有包都 build(-r = recursive)
pnpm -r --parallel test               # 所有包并行跑测试
pnpm add lodash --filter merchant     # 给指定包装依赖
pnpm why lodash                       # 谁依赖了 lodash

--filter 是日常用得最多的参数,支持包名、目录、git 变更范围等多种写法:

# 只构建"依赖了 ui 的包"(ui 改了,谁要重新构建?)
pnpm -r --filter '...@company/ui' build

迁移中的四个坑

1. 幽灵依赖现形记

老代码里有 import dayjs from 'dayjs',但 package.json 没声明——它一直靠的是某个依赖的依赖恰好把 dayjs 装进了扁平的 node_modules。pnpm 的严格隔离让这些全部报错。

解法:逐个补声明。虽然烦,但这是在排除定时炸弹。

2. 版本统一

admin 用 lodash 4.17.20,merchant 用 4.17.15,合并后以哪个为准?

我们借机统一到最新版,并用根目录的 pnpm.overrides 强制兜底:

{
  "pnpm": {
    "overrides": {
      "lodash": "^4.17.21"
    }
  }
}

3. 构建顺序

ui 没构建,admin 就起不来。pnpm 会根据依赖关系自动排拓扑序(topological order),只要依赖声明正确,pnpm -r build 就是对的顺序。这也是"依赖必须正确声明"的另一个理由。

4. CI 缓存 key

GitHub Actions 里缓存 pnpm store:

- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
  with:
    cache: 'pnpm'
- run: pnpm install --frozen-lockfile

安装时间从 3 分钟降到 40 秒。注意用 --frozen-lockfile 保证 CI 严格按 lock 装。

配套工具建议

  • changesets:多包版本管理和 changelog 生成,发版自动化必备
  • turborepo:构建缓存 + 任务编排,包多了以后(10+)再上,现在 5 个包 pnpm 原生够用

小结

  • pnpm workspace 的核心:workspace:* 引用本地包,改代码立即生效
  • 严格依赖隔离是特性不是麻烦,幽灵依赖迟早要还
  • --filter 是日常核心,-r 递归跑命令,拓扑序自动保证构建顺序
  • 版本冲突用 pnpm.overrides 统一,CI 记得缓存 pnpm store
  • 版本发布上 changesets,构建编排复杂了再上 turborepo

迁移一个月后的效果:组件 bug 修一处全平台生效、新功能组件一次开发三端复用、依赖总数下降了 30%。那次事故的教训,值了。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4