最近一年,来问我 Monorepo 的人明显变多了。不管是前端团队、后端团队还是做客户端的,几乎都会在某个阶段开始思考“要不要把代码塞进一个仓库里”。这个问题没有标准答案,但如果你连 Monorepo 到底是什么、它解决了什么问题、代价是什么都没理清楚,就直接上手 Lerna、Nx 或者 Turborepo,大概率会踩到比收益更多的坑。
这篇文章我把 Monorepo 这个概念从头到尾拆一遍:它是什么、不是什么,为什么大厂在用,落地时的工具怎么选,迁移时要注意什么,最后再把常见问题和排查技巧整理成一套实战清单。适合两类人看:一类是刚开始接触 Monorepo、被各种文章绕晕的同学,另一类是已经在多仓库模式下感受到痛点、想评估要不要往 Monorepo 迁的技术负责人。
1. Monorepo 到底是什么:把概念先拆干净
1.1 字面意思和实际含义,差别很大
Monorepo 是 monolithic repository 的缩写,直译过来是“单一代码仓库”。很多刚接触的人会误以为它就是把公司所有项目的代码一股脑塞进一个 Git 仓库,然后这个仓库会随着时间膨胀成一个谁都不敢碰的怪物。这是最常见,也最危险的理解偏差。
实际上的 Monorepo,强调的是“在同一个仓库里管理多个项目,同时保持项目之间的边界清晰”。它有四个关键特征:
- 代码确实都放在同一个版本控制仓库里;
- 每个子项目或子包仍然有自己独立的 package.json、构建配置、测试配置;
- 项目之间有明确的依赖关系声明,而不是靠目录结构来隐式耦合;
- 存在一套统一的工具链或规范来管理依赖安装、构建顺序和发布流程。
为什么这个区分很重要?因为如果你只是把多个仓库合并成一个,却没有引入 workspace 机制、没有梳理依赖、没有调整 CI,那你得到的只是一个大仓库(big repo),而不是 Monorepo。这种仓库往往比原来更难维护:所有代码互相可见,但没有任何边界约束,提交历史混乱,构建时间爆炸,最后团队只能靠“人品”维持秩序。
所以,我更愿意把 Monorepo 定义为一种开发和组织模式,而不是一个简单的“合并代码”动作。它真正的核心是“一个仓库,多个项目,统一管理,边界清晰”。理解到这层,后面所有工具选型和架构决策才有依据。
1.2 Monorepo 和 MultiRepo 的本质差异
和 Monorepo 相对的传统模式叫 MultiRepo,也叫 Polyrepo。这种模式下,每个项目一个独立仓库,常见于一个组织里多个团队各自为政的场景。比如组件库一个仓库,Web 应用一个仓库,管理后台一个仓库,移动端又单独一个仓库,每个仓库都有自己的 CI、依赖锁文件、发布流程和提交历史。
我整理过一张对比表,基本能说清二者差异:
| 维度 | MultiRepo | Monorepo |
|---|---|---|
| 跨项目改动 | 拆成多个 PR,存在先后依赖 | 一个 PR 可包含全部改动 |
| 内部依赖 | 发版后用版本号引用 | 可用 workspace 协议引用源码 |
| 代码复用 | 只能通过发布包复用 | 可直接复用源码,也可发布 |
| 权限粒度 | 精细到仓库 | 粗粒度,靠 CODEOWNERS 补充 |
| CI 效率 | 各仓库独立,容易重复建设 | 统一配置,可做增量构建 |
| 工具链成本 | 每个仓库各管各的 | 统一 dockerfile、lint、tsconfig |
| 维护体验 | 跨仓搜索、重构困难 | 全局搜索、重构方便 |
这套对比不是说 Monorepo 就一定更好。MultiRepo 在隔离性和权限控制上有天然优势,小团队、业务边界清晰、团队协作不频繁时,多仓库成本更低。关键是你要知道自己当前处于什么阶段,以及瓶颈到底在哪里。
1.3 Monorepo 并不是新东西,它的演进有迹可循
Monorepo 从时间线上看并不新鲜。早些年 CVS、SVN 时代,集中式版本控制是主流,大家本质上就是一个服务端、一个大仓库。Git 流行起来之后,分布式、轻量级分支让多仓库模式成为默认做法,因为每个仓库可以独立演进,效率很高。
但随着大型工程形态的出现,Google、Meta、Microsoft 这些公司内部其实长期使用 Monorepo(或者类似形态),用来自治跨项目一致性、统一依赖版本、支撑大规模重构。它们投入了大量基础设施来解决“仓库大导致构建慢、clone 慢”的问题,比如 Google 的 Bazel 配合远程缓存,内部还有专门的文件系统和代码搜索服务。
近几年,前端工程化领域把 Monorepo 的门槛打了下来,Lerna、Yarn workspaces、pnpm、Turborepo、Nx 这些工具让中小团队也能用很低的成本落地 Monorepo。所以它不是一个新概念,而更像是集中式思想在 Git 时代的一次高配回归,只是这次换了一套更成熟的工具体系。
2. Monorepo 的核心价值:为什么大家愿意折腾
2.1 原子提交:改动和依赖永远在一起
Monorepo 最核心、最难以替代的价值,是原子提交(atomic commit)。这个词听起来学术,但场景非常接地气。
假设你有一个组件库 package-a,和一个使用它的业务项目 app-b。今天你想改组件库的某个 API,同时把 app-b 的调用逻辑一起改掉。在 MultiRepo 模式下,你要经历:先改组件库,提交,发一个 canary 版本;然后去 app-b 的仓库里把依赖升级到 canary 版本,再提交。中间只要有一步断了,比如 app-b 的 CI 配置没及时更新,或者组件库版本发布失败,整个改动就会挂在那里。
Monorepo 里没有这个问题。一个 commit 可以同时包含组件库源码、调用方代码、测试和文档。CI 在同一个任务里就能把相关项目全部验证一遍。如果出问题,回滚也简单,一个 commit 整体回滚,不会出现“API 代码已经回滚了,但调用方已经合入新写法”的中间状态。
这个能力在大规模重构时尤其值钱。比如你要在整个前端团队推动一场断崖式 API 升级,Monorepo 里可以一次性完成,配合 IDE 的全局重命名,改动范围可见、可控。
2.2 依赖和版本:一套依赖树,不再各管各的
MultiRepo 下最常见的痛苦是依赖不一致。A 项目用的 React 17,B 项目用的 React 18,公共组件库可能又锁了另一个版本。一旦出现线上 bug,排查是不是版本差异导致的问题,往往比修复 bug 更费时间。
Monorepo 配合 workspace 机制后,内部包之间的依赖可以直接指向源码目录,开发阶段不需要反复发布包。比如用"@my-lib/utils": "workspace:*"这种写法,pnpm 会把依赖软链到本地源码目录一点,你改完 utils 的源码,引用方马上就能感知,不需要发布临时版本。
同时,Monorepo 会在根目录生成一个统一的锁文件,比如 pnpm 的 pnpm-lock.yaml。这意味着整个仓库的所有依赖都基于同一份解析结果安装,版本冲突可以在合并代码时被更早暴露出来。减少“本地能装、线上不能装”“你机器上跑得到、我机器上不行”这类玄学问题。
2.3 跨项目复用与大范围重构
如果没有 Monorepo,代码复用的路径通常是“把公共代码抽成包,发布到私有 npm 仓库,然后在各个项目里引用”。这条路能走通,但有个致命问题:发布太重了。你改一个工具函数,哪怕只改一行,也要走完整的版本发布、文档更新、通知下游升级流程。改的多了之后,团队会下意识避免动公共代码,公共代码就会慢慢腐化。
Monorepo 里,内部复用的成本被降到极低。你可以直接在源码层面引用另一个包,改完就能在同一个仓库的测试里验证所有调用方有没有被破坏。这种低成本复用带来的变化是结构性的:团队更愿意抽公共层,代码重复率降低,架构演进更快。
还有一个容易被忽视的点:跨项目的“可见性”。在 Monorepo 里,你写代码时能很容易发现“这个项目已经有人写过类似逻辑了”“底层 API 已经改过了”,而不是等到上线前才发现两套实现互相冲突。
2.4 CI 和工具链的集中优化空间
统一 CI 配置是 Monorepo 的隐藏收益。在 MultiRepo 时代,每个仓库都要维护一份流水线配置,docker 镜像版本可能还不一样,构建工具版本也参差不齐。排障时经常发现是“仓库的 CI 环境不一样”。
Monorepo 可以在一套配置里定义 lint、test、build、publish 流程,一次搭好,所有项目共用。更重要的是,它可以做到“按变更范围跑流水线”:只改 A 包,就没必要把 B、C、D 包全部重新构建一遍。只要工具能识别出受影响的包,就能大幅减少无效构建。
这一点也是后面 Turborepo、Nx 这些任务编排工具存在的意义:它们通过依赖图和内容哈希,把“哪些任务要跑、哪些可以跳过、哪些可以直接用缓存”计算出来,从根上解决“仓库变大后 CI 慢”的瓶颈。
3. 硬币的另一面:Monorepo 的隐藏成本
3.1 性能和规模门槛:不是所有团队都能承受
说完优点,必须泼一盆冷水。Monorepo 最大的争议就是性能。仓库文件多到一定程度后,git clone 时间变长,文件监听变慢,IDE 索引卡顿,CI 上每次全量构建的时长也会让人崩溃。
但这里有个常见的误区:很多团队还没到那个规模,就开始担心性能问题。几十个包、几千个文件,对现代工具链来说其实不算什么。真正到了百万级文件、几千个包的时候,才需要专门设计,比如 Git sparse-checkout 只检出需要的目录,或者引入远程缓存、分布式构建系统。
对大多数中小团队来说,更现实的性能问题来自“不会用增量构建”和“工具链没有配置缓存”。同样一个 Monorepo,直接跑全量构建和用 Turborepo 的增量缓存,差距可能是一个小时和十分钟。
3.2 权限与信息过载:代码全在一个仓库,归属感会变弱
Monorepo 默认所有人都能看到所有代码。这对开源项目无所谓,但在商业环境里,可能有些代码不希望所有团队随便看。虽然可以用 CODEOWNERS 做 code review 权责划分,也可以用分支保护限制关键目录的合并权限,但这些都属于“流程约束”,而不是硬隔离。
信息过载是更隐蔽的问题。仓库变得很大之后,新人入职想搞懂“这个项目怎么跑起来”,可能得先翻很久的文档。上千个 PR 同时流转,噪音也会变大。这时候就需要一批高水平的工程负责人来维护目录规范、文档索引和 code owner 清单,否则仓库会以肉眼可见的速度腐烂。
很多团队忽略了这个“治理成本”,以为把代码都放进来就完事了。实际上 Monorepo 对组织纪律的要求,比 MultiRepo 高出不少。
3.3 工具链复杂度:Monorepo 不是装一个 Lerna 就完事
很多团队一提到 Monorepo 就想到装 Lerna。但真实落地时,你需要考虑的不只是一个工具:
- 包管理层面:用什么支持 workspace?pnpm、yarn、还是 npm?
- 构建编排层面:怎么识别受影响的包?怎么缓存构建结果?
- 发布策略:内部包怎么发版?版本号怎么同步?要不要用 changesets?
- 依赖使用规范:能不能直接跨包 import 源码?哪些目录属于公共层?
- lockfile 策略:根目录统一锁文件,是否会影响各业务线的独立发布?
这一套组合拳学习成本不低。我看到不少团队在迁移前期非常兴奋,装了一堆工具,配置了各种花哨规则,结果三个月后没人能维护,仓库变成了新的历史包袱。
所以,我的一个观点是:Monorepo 工具链应该“金字塔式”地上。先从最基础的 pnpm workspace 开始,解决依赖问题;不够了再加一层 Turborepo 或 Nx,解决构建编排;还不够,再考虑远程缓存或 Bazel。不要一上来就把所有重型武器都架好。
3.4 团队协作规范:比代码本身更难定
Monorepo 更像是一套“大家一起维护的公共代码库”,任何一个包的重大变更都可能影响整个仓库。所以你需要更强的变更管理意识:
- 公共包必须有变更集(changeset),不能随手改 API 不通知下游;
- 目录边界不能随意跨越,业务逻辑不能随手放到公共包里;
- 依赖方向要尽量单向,不能让底层 package 去依赖上层应用;
- CI 必须在变更发生时快速反馈,不能跑到最后一步才报错。
这些规范看起来“软”,实际上决定了 Monorepo 能不能长期健康。没有规范的 Monorepo 很快会退化成一个互相乱依赖的大杂烩,比 MultiRepo 还难维护。
4. 落地工具选型:其实不一定要用最复杂的
4.1 主流方案横向对比
Monorepo 的工具生态可以用“你中有我、我中有你”来形容,非常容易让人选择困难。我按自己的实践,把主流方案拆成几个层次:
| 工具 | 核心定位 | 擅长的事 | 注意点 |
|---|---|---|---|
| Lerna | 老牌 monorepo 工具 | 版本管理、publish 流程 | 任务编排能力弱 |
| pnpm workspace | 包管理器层面的 workspace | 依赖安装快、严格隔离 | 不解决构建编排 |
| Yarn workspaces | 包管理器层面的 workspace | 常用、生态成熟 | 严格度稍弱于 pnpm |
| Turborepo | 任务编排与缓存 | 增量构建、远程缓存 | 不做发布管理 |
| Nx | 完整工程化平台 | 生成器、依赖图、affected 分析 | 学习成本高 |
| Bazel | 通用构建系统 | 超大规模、多语言构建缓存 | 配置成本极高 |
这里要强调一点:这些工具不是互斥的。实际场景中,pnpm workspace 负责依赖管理,Turborepo 负责任务编排,changesets 负责版本发布,已经成为一个非常常见且性价比很高的组合。你不用“选一个就用到底”,而是要看它们各自解决什么问题。
4.2 最小可用方案:pnpm workspace 就够了
如果你是从零开始,或者只有两到三个相关度比较高的仓库要合并,我的建议是先别碰 Nx 和 Bazel,甚至连 Turborepo 都可以晚点再装。先用 pnpm workspace 把依赖和开发体验理顺。
具体来说,三步就可以完成最小配置:
- 在仓库根目录创建
pnpm-workspace.yaml:
packages: - "apps/*" - "packages/*"- 在内部包的 package.json 里,用 workspace 协议引用其他内部包:
{ "name": "@my/app", "dependencies": { "@my/utils": "workspace:*" } }- 在根目录统一执行
pnpm install,生成一个全局的pnpm-lock.yaml。
这一步做完,你就有了一个可以本地开发的最小 Monorepo。内部包改完源码,引用方立即生效,不需要发布临时版本。pnpm 的硬链接机制也会让安装速度快很多,磁盘占用显著下降。
4.3 中型项目提升效率:Turborepo 的任务编排
等包数量多起来,比如有五个以上应用、十几个公共包时,你会开始痛感知一个点:每次 CI 都是全量构建,很浪费。这时候可以上 Turborepo。
Turborepo 本身不管理依赖,而是接管“任务编排”。它通过读取每个包的依赖关系,以及文件内容哈希,来判断当前代码变更会影响哪些任务。如果某个任务依赖的输入文件没有变化,且缓存命中,就直接从缓存恢复结果,不真正执行构建。
一个典型的 turbo.json 是这样:
{ "$schema": "https://turbo.build/schema.json", "globalDependencies": [".env"], "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["^build"], "outputs": [] } } }关键是dependsOn: ["^build"],它表示“当前包的 build 任务,依赖其所有内部依赖包的 build 任务先执行”。这样 Turborepo 就可以按拓扑顺序从底层依赖开始构建,同时跳过没有受影响的包。
加了远程缓存之后,整个团队的构建还能共享缓存历史数据:同一个 commit 在 CI 上构建过一次,其他开发者本地跑同样的任务可以直接拉取缓存结果,秒级完成。这个体验提升非常明显。
4.4 什么时候才要上 Nx 或 Bazel
Nx 和 Bazel 属于更重的方案,不建议普通团队一上来就选。
Nx 更像一个完整工程化平台,除了任务编排,还有代码生成器、依赖图可视化、affected 分析、plugins 体系。如果你所在的组织已经有很多项目,且希望提供统一开发体验,比如“一键生成一个新包”“自动生成测试文件”“依赖图可视化”,那 Nx 会很强大。但它的学习曲线很陡,配置也比 Turborepo 多。
Bazel 则是另一个量级的工具。它适用于多语言、超大规模仓库,比如一个仓库里同时有 C++、Java、Python、Android、iOS,并且对构建缓存、并发、可复现性有极高要求。普通 Web 项目上 Bazel,基本是杀鸡用牛刀,维护成本会把你拖垮。
我个人的判断标准是:包数量少于 20 个,先 pnpm workspace;超过 20 个或对增量构建有强诉求,加 Turborepo;需要完整工程化平台,再评估 Nx;多语言超大仓,才轮到 Bazel 出场。
5. 实操过程:从 MultiRepo 迁移到 Monorepo 的完整路径
5.1 迁移前先盘点,别急着搬代码
很多团队失败,是因为一上来就把所有仓库复制到一个目录下,然后开始改脚本。我建议的起点是“盘点”:
- 画出当前仓库之间的依赖关系图,搞清楚哪些包被谁依赖;
- 识别哪些属于内部公共包,哪些属于独立部署的应用;
- 明确边界:应用放进
apps/,内部库放进packages/,不相关、独立发布周期很长的项目不要强行纳入; - 记录当前各个包使用的依赖版本,迁移后统一版本策略。
这一步看起来繁琐,但能帮你避免“把所有东西都放进来,结果谁也不知道这个仓库怎么维护”的尴尬。
5.2 搭建 workspace 并调整依赖
具体迁移步骤可以这样拆:
- 新建根目录,初始化 git 仓库和 package.json;
- 创建
pnpm-workspace.yaml,声明apps/*和packages/*; - 把原有项目移动到对应目录;
- 把内部依赖从
"@my/utils": "1.2.3"改成"@my/utils": "workspace:*"; - 移除每个子仓库自己的 lockfile 和 node_modules;
- 在根目录执行
pnpm install,生成统一的pnpm-lock.yaml; - 把原来通过相对路径互相引用的代码,改成通过包名 import。
第 7 步比较容易被忽略。有些项目以前是通过../../packages/utils/src/index.ts这种相对路径引用的,这种引用方式在 Monorepo 里会让工具链很难判断依赖关系。改成包名引用之后,pnpm 和 Turborepo 才能正确识别依赖图,增量构建才有基础。
5.3 配置 CI 与缓存策略
迁移到 Monorepo 之后,CI 配置要有意识地“缓存”和“增量”。
首先是包管理器的缓存。pnpm 的 store 可以缓存很多依赖,CI 上一定要把node_modules或 pnpm store 缓存住,否则每次安装都要全量拉取,时间会很难看。
其次是构建缓存的配置。如果用了 Turborepo,可以在 CI 上启用远程缓存。例如在 GitHub Actions 里,你可以把node_modules/.cache/turbo作为缓存路径;如果团队规模大,也可以接入 Turborepo 的 remote cache 服务或者自建。
最后是任务的粒度。不要每次提交都在所有包上跑所有任务。尽量用turbo run lint test build --filter=[HEAD^]这样的命令,让 CI 只处理受影响的包。配合“只有主分支和 release 分支才做全量构建”的策略,可以有效控制成本。
5.4 我在迁移过程中踩过的三个坑
第一个坑:没有先改用 workspace 协议,还是用的发布版本号。结果本地开发时 A 包依赖的是本地源码,CI 上却被解析成了 registry 上的旧版本,导致“本地过、线上挂”。这个坑出现频率极高,所以迁移时一定要先统一 internal dependencies 的引用方式。
第二个坑:依赖提升问题。pnpm 的默认 node_modules 结构和传统的 npm/yarn 不一样,它不是完全扁平化的。一些老项目习惯在代码里直接 import 一个没有在 package.json 里声明的“隐式依赖”,迁移之后直接报Cannot find module。遇到这种情况,可以通过配置.npmrc来临时解决:
public-hoist-pattern[]=*@types/* public-hoist-pattern[]=*eslint*但要注意,这只是止疼药。真正的做法是让每个包显式声明它自己的依赖,不要利用“幽灵依赖”。
第三个坑:把带着大量历史二进制文件的老项目迁进来,导致 git 仓库体积暴涨。比如有些仓库里有旧的图片、视频、安装包,这些一旦进 git 历史,clone 时间会不可逆地变长。如果只是普通文本代码,问题不大;但如果已经发现了大文件在历史里,需要尽早用工具清理,或者改用 Git LFS 管理大文件。
6. 常见问题与排查技巧实录
6.1 幽灵依赖:报错时多半是依赖没声明
Monorepo 迁移后最常见的报错之一,就是Cannot find module 'xxx'。原因往往是一个包引入了自己没有在 package.json 里声明的第三方依赖。在原来扁平化的 node_modules 结构下,这种隐式依赖可能碰巧能工作;但在 pnpm 的严格隔离模式下,就会立刻暴露。
解决思路:找到报错的那个包,把它真正用到的依赖都写进 package.json。如果第三方库之间还有复杂的 peerDependencies 关系,也需要显式声明。临时可以通过public-hoist-pattern缓解,但长远看还是要把依赖关系理顺。
6.2 循环依赖:构建能过,运行时报错
循环依赖在 Monorepo 里更隐蔽。比如 app 依赖 utils,utils 又依赖 app 里的某个类型,打包器可能不报错,但运行时会出现Cannot access before initialization或者莫名其妙的 undefined。
排查方法很简单但有效:用madge --circular这样的工具扫描目录,把循环依赖找出来。修复的核心思路不是“破解”循环,而是“打破循环”。把两个包共同依赖的部分抽到更底层的包,让依赖方向变成单向的。
6.3 CI 时间没有变短,反而变长了
很多团队迁移 Monorepo 后第一周都会困惑:为什么 CI 更慢了?大概率是这三个原因之一:
- 没有用增量构建,每次提交把所有包装全部构建一遍;
- 没有缓存依赖安装,每次 CI 都要重新 install;
- 迁移后依赖数量变多、文件变更范围变大,但任务编排没有跟上。
对照检查:是否引入了 Turborepo/Nx 这类任务编排?是否有远程缓存或者 CI 缓存?是否只在受影响的包上跑任务?如果都没有,那就不是在用 Monorepo,只是在用一个更大的 MultiRepo。
还有一个容易被忽略的点:如果团队成员习惯每天频繁 rebase,代码变更的哈希计算会变得不稳定,缓存命中率会降低。Monorepo 团队最好建立“小步提交+主干开发”的节奏,减少无谓的分支同步。
6.4 多语言 Monorepo 的注意事项
Monorepo 并不是前端的专利。不少后端团队也想把 Go、Python、Java 服务放在同一个仓库里,实现统一的配置管理和跨服务重构。
多语言 Monorepo 的真实挑战是“没有统一的包管理和构建工具”。Java 用 Maven/Gradle,Python 用 poetry/pip,Go 用 module,前端用 pnpm/yarn。你可以让它们共享同一个 git 仓库,但它们的依赖解析、构建缓存、任务编排很难统一。
这种情况下,我建议的方案是:仓库层只负责目录规范和版本管理,用根目录的 Makefile 或 Taskfile 定义统一的任务入口;各个语言的项目在自己的目录里用自己的工具链。只有当跨语言共享构建结果成为明确需求时,才考虑 Bazel 这种通用构建系统,但同时要接受它高昂的配置和运维成本。
7. 别把 Monorepo 当万能药:适用性判断
7.1 哪些场景真的不适合
Monorepo 有价值,但确实不是所有场景都适合。我见过的一些反例:
- 多个团队的产品线完全独立,互相依赖很少,发布节奏也不一致,强行合成一个仓库只会增加协调成本;
- 仓库里有大量二进制资产、大文件,git 管理会非常痛苦,除非配合 LFS 或外部存储;
- 工程能力薄弱,连统一的 lint、测试、构建规范都没有,Monorepo 不会自动帮你建立规范,反而会让混乱更集中;
- 组织对权限隔离有硬性要求,某些代码不能开放给其他团队查看,Monorepo 很难做硬隔离。
判断标准其实很简单:如果你频繁遇到跨项目改动、公共代码复用、版本同步问题,Monorepo 大概率值得尝试;如果你只是觉得“大家都在用”,那最好先等等。
7.2 组织架构和代码仓库的匹配
康威定律一直很扎心:系统结构会复制组织的沟通结构。Monorepo 适合“多个团队围绕同一产品线、互相依赖频繁”的组织,因为跨项目改动很常见,代码就适合放在一起;但完全独立的事业部或跨 BU 项目,硬合成一个仓库会让协作成本远大于收益。
我见过一个还不错的折中方案:把业务域相关的几个仓库合并成一个“小 Monorepo”,比如钱包团队把组件库、BFF、运营后台放一起;而整个公司层面仍然是多仓库。这种“模块化 Monorepo”既享受了跨项目改动和统一工具的收益,又避免了超大仓库带来的性能和组织摩擦。
所以,落地方案不一定是“公司级一个大仓库”,也可以是“团队级一个中仓库”。先想清楚团队边界,再决定仓库边界,通常能把 Monorepo 的优势发挥出来,同时把代价控制在可接受范围。
7.3 Monorepo 后续的演化方向
工具链还在快速变化,但有几个趋势比较明显:
- 模块化 Monorepo 会成为主流,团队按业务域拆分多个独立但内部使用 Monorepo 的仓库;
- 远程缓存和内容寻址构建会越来越普及,缓存命中率会成为开发者体验的重要指标;
- 包级发布和仓库级发布会共存,内部用源码引用,对外仍按包发布,兼顾效率和契约;
- 工程平台化,Nx 这类工具提供的生成器、依赖图、自动化迁移能力,会让 Monorepo 的使用门槛进一步降低。
与其纠结选哪个新框架,不如记住 Monorepo 的三条核心原则:依赖关系清晰、构建可以缓存、发布流程可控。只要这三条守住,工具怎么换都不会偏。
我个人在实际操作中的体会是,Monorepo 的价值不是来自“把代码放一起”,而是来自“把依赖关系和构建流程理顺”。如果你能做到这一点,哪怕暂时只用 pnpm workspace,体验已经比一堆相互依赖却各自维护的多仓库好上太多。我最早负责迁移的时候,也只是先用 pnpm workspace 把两个耦合度最高的仓库合起来,连 Turborepo 都没装,先把依赖关系理清楚了,CI 就已经稳定了很多;后来又加了 turbo,构建时间从十几分钟降到几分钟。这个过程的教训就是:不要为了用工具而用工具,先解决依赖和边界问题,再谈花哨的缓存和编排。
最后再分享一个小技巧:如果你的团队还在犹豫要不要迁,可以先挑两个耦合度最高、联调最痛苦的仓库做“试点 Monorepo”。用 pnpm workspace 把它们合起来,跑一两个月,再拿数据说话:跨项目改动到底快了多少,发布频率有没有提升,CI 时长是不是可控。用实际数据做决策,比拍脑袋定方案靠谱得多。