pnpm Monorepo 工程化实践:从多仓库泥潭到一条命令发布

举报
yd_231768394 发表于 2026/11/01 22:54:19 2026/11/01
【摘要】 pnpm Monorepo 工程化实践:从多仓库泥潭到一条命令发布 前言当项目从 1 个仓库膨胀到 6 个(web、admin、mobile、ui、utils、sdk),跨仓库改动就成了灾难:改一个共享组件要发 3 次包、装 3 次依赖、走 3 次 CI。Monorepo 解决的正是这个问题,但配错了比多仓库更痛苦。本文给出一套能直接抄的 pnpm workspace + Turborep...

pnpm Monorepo 工程化实践:从多仓库泥潭到一条命令发布

前言

当项目从 1 个仓库膨胀到 6 个(web、admin、mobile、ui、utils、sdk),跨仓库改动就成了灾难:改一个共享组件要发 3 次包、装 3 次依赖、走 3 次 CI。Monorepo 解决的正是这个问题,但配错了比多仓库更痛苦。本文给出一套能直接抄的 pnpm workspace + Turborepo + Changesets 组合拳。

一、为什么是 pnpm

三点硬差异:

  1. 省磁盘、省安装时间:包内容存在全局 store,项目里的 node_modules 用硬链接指过去。十个项目共用一份 React,磁盘占用是 npm 的零头;
  2. 依赖结构严格:npm/yarn 会把没声明的依赖提升到顶层,你 import lodash 即使 package.json 里没写也能跑——这叫"幽灵依赖",一旦某个中间包不再依赖它,构建立刻炸。pnpm 的 node_modules 只暴露你声明过的包;
  3. 天生为 workspace 设计:workspace: 协议直接软链本地包,改源码即时生效,不需要 npm link 那套玄学。
corepack enable && corepack prepare pnpm@9 --activate

根 package.json 里写 "packageManager": "pnpm@9.15.0",团队和 CI 才不会各装各的版本。

二、目录结构与 workspace 声明

repo/
├── pnpm-workspace.yaml
├── package.json
├── apps/
│   ├── web/
│   └── admin/
└── packages/
    ├── ui/
    ├── utils/
    └── tsconfig/          # 共享配置包
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

catalog:
  react: ^19.0.0
  typescript: ^5.6.0

catalog(pnpm 9.5+)统一管理跨包版本,子包写 catalog: 即可,升级 React 只改一行:

// apps/web/package.json
{
  "name": "@repo/web",
  "dependencies": {
    "@repo/ui": "workspace:*",
    "react": "catalog:"
  }
}

workspace:* 在发布时会被替换成真实版本号,本地开发则是符号链接——这是 Monorepo 的核心机制。

三、依赖管理的三个坑

  1. 根目录只放工程工具:eslint、prettier、turbo 放根 package.json;业务依赖放各子包。根目录什么都装,等于把所有包强行耦合;
  2. shamefully-hoist=true 是止痛药不是解药:它能救活不兼容 pnpm 的老包,但会把幽灵依赖重新引回来,只在迁移期临时开;
  3. peerDependencies 必须显式声明:packages/ui 依赖 React 时写进 peerDependencies,否则最终产物可能打进两份 React,报出 “Invalid hook call”。

排查依赖去向:

pnpm why react --filter @repo/web
pnpm list --depth 0 -r

四、任务编排:别再手动 cd

pnpm -r build                     # 所有包按拓扑序构建
pnpm --filter @repo/web... build  # web 及其全部上游依赖
pnpm --filter @repo/web dev       # 只跑一个

... 后缀表示"包含依赖",是 Monorepo 里最常用的过滤器。

规模再大就该上 Turborepo,核心价值是缓存——同样的输入不重复执行:

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test": { "dependsOn": ["build"], "outputs": [] },
    "lint": { "outputs": [] },
    "dev": { "cache": false, "persistent": true }
  }
}
  • ^build:先构建依赖包再构建自己,拓扑序自动推导,不用手写顺序;
  • outputs 声明产物路径,命中缓存直接还原文件:冷启动 3 分钟,改一行样式后 4 秒;
  • CI 上配 TURBO_TOKEN 走远程缓存,团队共用一份产物。

五、发布:用 Changesets 而不是手改版本号

pnpm add -Dw @changesets/cli
pnpm changeset          # 交互式选受影响包 + 版本级别 + 变更说明
pnpm changeset version  # 批量升版本、改 CHANGELOG、更新内部依赖范围
pnpm changeset publish  # 发 npm 并打 tag

.changeset/config.json 里把 "updateInternalDependencies": "patch" 配上,内部的 workspace:* 才能被正确改写,避免发布出去的包还引用本地路径。

六、CI 上的三个提速点

手段 做法 典型效果
依赖缓存 缓存 pnpm store,而不是 node_modules 安装 90s → 10s
增量构建 --filter=...[origin/main] 只跑受影响包 耗时随改动范围下降
并发任务 Turborepo 默认并行跑无依赖关系的任务 吃满 CI 机器核数
pnpm --filter=...[origin/main] run test

注意缓存 key 要包含 lockfile 哈希,否则改了依赖还在用旧缓存,会排查到怀疑人生。

总结

Monorepo 的收益不在"少开几个窗口",而在原子化变更:一次 PR 同时改组件库和两个应用,CI 全量验证后一起上线。

落地顺序建议:pnpm workspace 跑通 → catalog 统一版本 → Turborepo 加缓存 → Changesets 管发布。每步单独验证,别一次性上全家桶——出问题时你分不清是谁的锅。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。