你肯定遇到过这种情况:项目里同时有前端、后端、公共组件,放在一个仓库嫌乱,拆成多个仓库又改一个公共函数要在五个项目里各改一遍。于是出现了 Monorepo、Turborepo、pnpm、Changesets 这四个词。它们不是互相替代,而是分别解决工程化中不同层面的问题。读完之后,你会明白它们各自解决什么、技术原理是什么、彼此之间是什么关系,以及在实际项目中该如何组合使用。
前端工程化发展到今天,一个中型项目往往包含多个应用(Web、小程序、Node 服务)和多个共享包(UI 组件库、工具函数、类型定义)。传统的多仓库(Polyrepo)模式有两个致命痛点:
react、lodash,磁盘空间爆炸,版本不同步还容易出 bug。于是工程界开始借鉴谷歌、Facebook 的做法,把多个项目放进同一个仓库——这就是 Monorepo。但光放进去还不够,你还需要:
这四个工具不是互相替代,而是互补的,分别解决前端工程化中不同层面的问题。
Monorepo 是一种代码仓库组织策略,在一个 Git 仓库里管理多个相互独立但又相互依赖的项目(应用、库、服务)。
| 维度 | Polyrepo(多仓库) | Monorepo(单仓库) |
|---|---|---|
| 代码复用 | 发布 npm 包或复制粘贴 | 直接通过 workspace 引用源码 |
| 依赖管理 | 每个仓库独立安装依赖,重复浪费 | 依赖提升到根目录,一处安装全局使用 |
| 跨项目改动 | 改一个公共函数需改 N 个仓库 | 只需改一次,所有项目立即生效 |
| 权限控制 | 按仓库隔离,精细但麻烦 | 可通过 CODEOWNERS 实现目录级权限 |
| CI/CD | 每个仓库单独构建,资源分散 | 只构建受影响的项目,可并行执行 |
| 学习成本 | 低,各项目独立 | 需理解 workspaces、任务编排等概念 |
my-monorepo/
├── apps/ # 应用程序
│ ├── web/ # React 前端
│ ├── admin/ # 后台管理系统
│ └── api/ # Node 后端
├── packages/ # 共享包
│ ├── ui/ # 组件库
│ ├── utils/ # 工具函数
│ └── config/ # 共享配置(ESLint、TS)
├── package.json
├── pnpm-workspace.yaml # 工作区配置
└── turbo.json # Turborepo 配置
package.json 声明的包(因为被提升到了根目录),导致部署时遗漏依赖。pnpm 是一个高性能的包管理器,它通过内容可寻址存储和符号链接实现多项目间依赖的全局去重,比 npm/yarn 更快、更省磁盘空间。
| 维度 | npm / yarn(传统) | pnpm |
|---|---|---|
| 依赖存储 | 每个项目 node_modules 都复制一份依赖 |
全局 store 存储一份,通过硬链接复用 |
| 磁盘占用 | 100 个项目 = 100 份 react | 100 个项目 = 1 份 react |
| 安装速度 | 慢,重复下载 | 快,已下载过的直接从缓存链接 |
| 幽灵依赖 | 存在(项目可访问未声明的包) | 不存在,严格的依赖隔离 |
| Monorepo 支持 | 需要 workspaces 配置 |
原生支持,通过 pnpm-workspace.yaml
|
在项目根目录创建 pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
然后执行 pnpm install。pnpm 会自动把 apps/ 和 packages/ 下的每个子目录当作一个 workspace 包,并通过符号链接让它们互相引用。
pnpm install # 安装所有依赖
pnpm add react -w # 给根目录添加依赖(-w 表示 workspace root)
pnpm --filter web add lodash # 只给 web 应用添加 lodash
pnpm --filter web dev # 只运行 web 应用的 dev 脚本
node_modules 是平铺结构,在 pnpm 下可能不工作(可通过 shamefully-hoist 解决)。--filter、workspace 协议("ui": "workspace:*")等概念。Turborepo 是一个高性能的任务编排器,专门为 Monorepo 设计。它会缓存每个任务的输入输出,第二次运行相同输入时直接跳过执行,从而实现秒级重构建。
npm run 脚本有什么区别?| 维度 | 普通脚本 | Turborepo |
|---|---|---|
| 执行方式 | 按顺序串行执行 | 自动并行执行(依赖关系不变) |
| 缓存 | 无 | 内容寻址缓存,相同输入直接返回缓存结果 |
| 增量构建 | 需要手动实现 | 自动检测哪些项目变了,只构建受影响的部分 |
| 远程缓存 | 不支持 | 支持云缓存,团队成员共享构建缓存 |
| 依赖感知 | 无 | 自动识别 dependsOn,按拓扑顺序构建 |
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 输出、环境变量)的哈希值。如果哈希值没有变化,直接输出之前缓存的产物,执行时间从分钟级降为毫秒级。
node_modules),缓存命中率会很低。Changesets 是一个用于 Monorepo 的版本管理和 changelog 生成工具。它让你在提交代码时记录变更意图,然后一键批量发布所有需要升级的包。
在 Monorepo 中,你改了 packages/utils,可能会同时影响 apps/web 和 apps/admin。如果手动去修改这些包的 package.json 版本号,并各自生成 changelog,非常繁琐且容易漏。Changesets 自动化了这个流程。
开发者改代码
↓
pnpm changeset # 交互式选择要升级的包、填写变更描述
↓
生成 .changeset/*.md 文件(提交到 Git)
↓
CI / 发布时运行 pnpm changeset version
↓
自动升级版本号、更新 changelog、删除 .changeset 文件
↓
pnpm publish -r # 发布所有变更的包到 npm
version 命令并提交,需要配置 GitHub Actions 或 GitLab CI。updateInternalDependencies。| 工具 | 角色定位 | 解决的核心问题 | 类比 |
|---|---|---|---|
| Monorepo | 代码组织策略 | 多个项目如何放进同一个仓库 | 盖一栋大楼(框架) |
| pnpm | 包管理器 | 如何快速、节省空间地安装依赖 | 大楼的水电管道系统 |
| Turborepo | 任务编排器 | 如何加速构建、测试、lint 等任务 | 大楼的电梯调度系统 |
| Changesets | 版本管理 | 如何自动化发版和生成 changelog | 大楼的物业管理系统 |
它们的协作关系:
开发者修改代码(在 Monorepo 中)
↓
pnpm 负责安装依赖,链接 workspace 包
↓
Turborepo 负责按需执行任务(build、test、lint),利用缓存加速
↓
开发完成后,提交 PR
↓
PR 合并到 main 分支
↓
CI 运行 Changesets:自动升级版本、生成 changelog
↓
pnpm publish -r 发布到 npm
pnpm + Monorepo 就够了,不需要 Turborepo(构建不慢)和 Changesets(手动改版本号也能接受)。build。pnpm + Turborepo + Monorepo,用 Turborepo 的缓存和并行能力加速 CI。pnpm + Turborepo + Changesets + Monorepo,全套上齐。changeset version 和 publish。pnpm import 把现有 package-lock.json 转成 pnpm-lock.yaml;然后逐步把相关仓库移入 packages/,调整 import 路径;最后引入 Turborepo 优化 CI。回到最开始的问题:为什么需要 Monorepo、Turborepo、pnpm、Changesets 这四个工具?
它们不是 “银弹”,但当你团队规模膨胀、项目耦合加深时,这套组合拳能让你从 “复制粘贴工程师” 进化为 “工程化架构师”。
如果你也在搭建 Monorepo,或者被多仓库的代码复用问题折磨过,点个赞让我看到。赞多的话,下一篇写 “如何从零落地一个 pnpm + Turborepo + Changesets 的 Monorepo 项目,包含完整 CI 配置”。