一口气讲清楚 Monorepo、Turborepo、pnpm、Changesets 到底是什么?

0 阅读9分钟

你肯定遇到过这种情况:项目里同时有前端、后端、公共组件,放在一个仓库嫌乱,拆成多个仓库又改一个公共函数要在五个项目里各改一遍。于是出现了 Monorepo、Turborepo、pnpm、Changesets 这四个词。它们不是互相替代,而是分别解决工程化中不同层面的问题。读完之后,你会明白它们各自解决什么、技术原理是什么、彼此之间是什么关系,以及在实际项目中该如何组合使用。


一、先搞清楚一件事:为什么会有这些工具?

前端工程化发展到今天,一个中型项目往往包含多个应用(Web、小程序、Node 服务)和多个共享包(UI 组件库、工具函数、类型定义)。传统的多仓库(Polyrepo)模式有两个致命痛点:

  1. 代码复用难:改一个公共函数,要在 5 个仓库里各改一遍,还要各自发版。
  2. 依赖管理乱:每个仓库都重复安装 reactlodash,磁盘空间爆炸,版本不同步还容易出 bug。

于是工程界开始借鉴谷歌、Facebook 的做法,把多个项目放进同一个仓库——这就是 Monorepo。但光放进去还不够,你还需要:

  • 一个包管理器来高效处理依赖(pnpm)
  • 一个构建编排器来加速构建和任务执行(Turborepo)
  • 一个版本管理工具来帮你自动发版和生成 changelog(Changesets)

这四个工具不是互相替代,而是互补的,分别解决前端工程化中不同层面的问题。


二、Monorepo:把多个项目放进同一个“家”

2.1 一句话定义

Monorepo 是一种代码仓库组织策略,在一个 Git 仓库里管理多个相互独立但又相互依赖的项目(应用、库、服务)。

2.2 跟 Polyrepo 有什么区别?

维度Polyrepo(多仓库)Monorepo(单仓库)
代码复用发布 npm 包或复制粘贴直接通过 workspace 引用源码
依赖管理每个仓库独立安装依赖,重复浪费依赖提升到根目录,一处安装全局使用
跨项目改动改一个公共函数需改 N 个仓库只需改一次,所有项目立即生效
权限控制按仓库隔离,精细但麻烦可通过 CODEOWNERS 实现目录级权限
CI/CD每个仓库单独构建,资源分散只构建受影响的项目,可并行执行
学习成本低,各项目独立需理解 workspaces、任务编排等概念

2.3 一个简单的目录结构

my-monorepo/
├── apps/              # 应用程序
│   ├── web/           # React 前端
│   ├── admin/         # 后台管理系统
│   └── api/           # Node 后端
├── packages/          # 共享包
│   ├── ui/            # 组件库
│   ├── utils/         # 工具函数
│   └── config/        # 共享配置(ESLint、TS)
├── package.json
├── pnpm-workspace.yaml # 工作区配置
└── turbo.json          # Turborepo 配置

2.4 技术挑战

  • 依赖提升带来的幽灵依赖:项目可能引用未在自身 package.json 声明的包(因为被提升到了根目录),导致部署时遗漏依赖。
  • 构建性能:随着项目增多,全量构建会越来越慢,需要增量构建和缓存。
  • 权限与协作:需要合理的 CODEOWNERS 和分支策略,避免一个人改崩整个仓库。

三、pnpm:比 npm 更聪明的包管理器

3.1 一句话定义

pnpm 是一个高性能的包管理器,它通过内容可寻址存储符号链接实现多项目间依赖的全局去重,比 npm/yarn 更快、更省磁盘空间。

3.2 跟 npm / yarn 有什么区别?

维度npm / yarn(传统)pnpm
依赖存储每个项目 node_modules 都复制一份依赖全局 store 存储一份,通过硬链接复用
磁盘占用100 个项目 = 100 份 react100 个项目 = 1 份 react
安装速度慢,重复下载快,已下载过的直接从缓存链接
幽灵依赖存在(项目可访问未声明的包)不存在,严格的依赖隔离
Monorepo 支持需要 workspaces 配置原生支持,通过 pnpm-workspace.yaml

3.3 怎么配置 pnpm workspace?

在项目根目录创建 pnpm-workspace.yaml

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

然后执行 pnpm install。pnpm 会自动把 apps/packages/ 下的每个子目录当作一个 workspace 包,并通过符号链接让它们互相引用。

3.4 常用命令

pnpm install                 # 安装所有依赖
pnpm add react -w            # 给根目录添加依赖(-w 表示 workspace root)
pnpm --filter web add lodash # 只给 web 应用添加 lodash
pnpm --filter web dev        # 只运行 web 应用的 dev 脚本

3.5 技术挑战

  • 原生工具链兼容性:一些旧的 npm 脚本或工具假设 node_modules 是平铺结构,在 pnpm 下可能不工作(可通过 shamefully-hoist 解决)。
  • 学习成本:开发者需要理解 --filter、workspace 协议("ui": "workspace:*")等概念。

四、Turborepo:让构建任务“快如闪电”

4.1 一句话定义

Turborepo 是一个高性能的任务编排器,专门为 Monorepo 设计。它会缓存每个任务的输入输出,第二次运行相同输入时直接跳过执行,从而实现秒级重构建。

4.2 跟普通 npm run 脚本有什么区别?

维度普通脚本Turborepo
执行方式按顺序串行执行自动并行执行(依赖关系不变)
缓存内容寻址缓存,相同输入直接返回缓存结果
增量构建需要手动实现自动检测哪些项目变了,只构建受影响的部分
远程缓存不支持支持云缓存,团队成员共享构建缓存
依赖感知自动识别 dependsOn,按拓扑顺序构建

4.3 Turborepo 工作原理

Turborepo 用 管道(pipeline) 定义任务之间的关系。一个典型的 turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],      // 先构建依赖包,再构建当前包
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false,               // 开发模式不缓存
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]       // 先跑依赖包的 lint
    },
    "test": {
      "dependsOn": ["build"]       // 先 build 再 test
    }
  }
}

缓存机制:Turbo 会计算任务输入(源代码、依赖的 task 输出、环境变量)的哈希值。如果哈希值没有变化,直接输出之前缓存的产物,执行时间从分钟级降为毫秒级。

4.4 技术挑战

  • 缓存失效过于保守:如果你的任务输入包含不必要的大文件(如 node_modules),缓存命中率会很低。
  • 远程缓存需要服务器:团队共享缓存需要自己部署或使用 Vercel 的 Remote Cache。

五、Changesets:版本管理不再痛苦

5.1 一句话定义

Changesets 是一个用于 Monorepo 的版本管理和 changelog 生成工具。它让你在提交代码时记录变更意图,然后一键批量发布所有需要升级的包。

5.2 为什么需要它?

在 Monorepo 中,你改了 packages/utils,可能会同时影响 apps/webapps/admin。如果手动去修改这些包的 package.json 版本号,并各自生成 changelog,非常繁琐且容易漏。Changesets 自动化了这个流程。

5.3 工作流程

开发者改代码
    ↓
pnpm changeset      # 交互式选择要升级的包、填写变更描述
    ↓
生成 .changeset/*.md 文件(提交到 Git)
    ↓
CI / 发布时运行 pnpm changeset version
    ↓
自动升级版本号、更新 changelog、删除 .changeset 文件
    ↓
pnpm publish -r    # 发布所有变更的包到 npm

5.4 技术挑战

  • 与 CI/CD 集成:需要在 PR 合并后自动运行 version 命令并提交,需要配置 GitHub Actions 或 GitLab CI。
  • 依赖升级的传递性:如果你改了底层包,上层包是否要强制升级?Changesets 可以自动处理,但需要正确配置 updateInternalDependencies

六、四者的关系:一张图讲清楚

工具角色定位解决的核心问题类比
Monorepo代码组织策略多个项目如何放进同一个仓库盖一栋大楼(框架)
pnpm包管理器如何快速、节省空间地安装依赖大楼的水电管道系统
Turborepo任务编排器如何加速构建、测试、lint 等任务大楼的电梯调度系统
Changesets版本管理如何自动化发版和生成 changelog大楼的物业管理系统

它们的协作关系:

开发者修改代码(在 Monorepo 中)
        ↓
pnpm 负责安装依赖,链接 workspace 包
        ↓
Turborepo 负责按需执行任务(build、test、lint),利用缓存加速
        ↓
开发完成后,提交 PR
        ↓
PR 合并到 main 分支
        ↓
CI 运行 Changesets:自动升级版本、生成 changelog
        ↓
pnpm publish -r 发布到 npm

七、技术选型指南:实际工程中怎么组合?

场景一:个人项目或小团队(2-5 人,3-5 个包)

  • 推荐pnpm + Monorepo 就够了,不需要 Turborepo(构建不慢)和 Changesets(手动改版本号也能接受)。
  • 操作:直接用 pnpm workspace,在根目录写几个 npm scripts 串行执行 build

场景二:中型项目(5-20 人,10-20 个包,构建耗时 > 2 分钟)

  • 推荐pnpm + Turborepo + Monorepo,用 Turborepo 的缓存和并行能力加速 CI。
  • 版本管理:可以暂时手动改版本,也可以用 Changesets 但非强制。

场景三:大型项目 / 开源库(多人协作,频繁发版,包之间有复杂依赖)

  • 推荐pnpm + Turborepo + Changesets + Monorepo,全套上齐。
  • 额外:配置远程缓存(如 Vercel Remote Cache)让团队成员共享构建结果;设置 CI 自动执行 changeset versionpublish

场景四:已有大量 npm 包,准备迁移到 Monorepo

  • 步骤:先用 pnpm import 把现有 package-lock.json 转成 pnpm-lock.yaml;然后逐步把相关仓库移入 packages/,调整 import 路径;最后引入 Turborepo 优化 CI。

💎 写在最后

回到最开始的问题:为什么需要 Monorepo、Turborepo、pnpm、Changesets 这四个工具?

  • Monorepo 给你一个容纳多项目的大房子。
  • pnpm 给你高效的管道系统,让依赖管理快如闪电。
  • Turborepo 给你智能的电梯,让构建任务不再重复劳动。
  • Changesets 给你规范的物业管理,让版本发布井井有条。

它们不是“银弹”,但当你团队规模膨胀、项目耦合加深时,这套组合拳能让你从“复制粘贴工程师”进化为“工程化架构师”。

如果你也在搭建 Monorepo,或者被多仓库的代码复用问题折磨过,点个赞让我看到。赞多的话,下一篇写“如何从零落地一个 pnpm + Turborepo + Changesets 的 Monorepo 项目,包含完整 CI 配置”。