你肯定遇到过这种情况:项目里同时有前端、后端、公共组件,放在一个仓库嫌乱,拆成多个仓库又改一个公共函数要在五个项目里各改一遍。于是出现了 Monorepo、Turborepo、pnpm、Changesets 这四个词。它们不是互相替代,而是分别解决工程化中不同层面的问题。读完之后,你会明白它们各自解决什么、技术原理是什么、彼此之间是什么关系,以及在实际项目中该如何组合使用。
一、先搞清楚一件事:为什么会有这些工具?
前端工程化发展到今天,一个中型项目往往包含多个应用(Web、小程序、Node 服务)和多个共享包(UI 组件库、工具函数、类型定义)。传统的多仓库(Polyrepo)模式有两个致命痛点:
- 代码复用难:改一个公共函数,要在 5 个仓库里各改一遍,还要各自发版。
- 依赖管理乱 :每个仓库都重复安装
react、lodash,磁盘空间爆炸,版本不同步还容易出 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 一个简单的目录结构
perl
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 份 react | 100 个项目 = 1 份 react |
| 安装速度 | 慢,重复下载 | 快,已下载过的直接从缓存链接 |
| 幽灵依赖 | 存在(项目可访问未声明的包) | 不存在,严格的依赖隔离 |
| Monorepo 支持 | 需要 workspaces 配置 |
原生支持,通过 pnpm-workspace.yaml |
3.3 怎么配置 pnpm workspace?
在项目根目录创建 pnpm-workspace.yaml:
yaml
packages:
- "apps/*"
- "packages/*"
然后执行 pnpm install。pnpm 会自动把 apps/ 和 packages/ 下的每个子目录当作一个 workspace 包,并通过符号链接让它们互相引用。
3.4 常用命令
bash
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:
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/web 和 apps/admin。如果手动去修改这些包的 package.json 版本号,并各自生成 changelog,非常繁琐且容易漏。Changesets 自动化了这个流程。
5.3 工作流程
bash
开发者改代码
↓
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 | 大楼的物业管理系统 |
它们的协作关系:
bash
开发者修改代码(在 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 version和publish。
场景四:已有大量 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 配置"。