Monorepo入门
1. Monorepo 介绍
核心价值:把“需要一起演进的一组项目”放在同一个版本空间里,从而让跨项目改动(API 变更、重构、升级)能在一次提交里完成并验证
Monorepo 是把多个相关项目/包放在同一个 Git 仓库中管理的策略,有助于跨项目联动修改、内部包共享更顺畅、统一规范与 CI、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。
Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。
2. Monorepo 演进
![]()
2.1 阶段一:单仓库巨石应用(Monolith)
初期很爽:一个仓库、一个 package.json、一个 node_modules、一个构建流程,但随着迭代业务复杂度的提升,项目代码会变得越来越多,越来越复杂,大量代码构建效率也会降低,最终导致了单体巨石应用,这种代码管理方式称之为 Monolith。
问题在于:业务一旦变大,就容易出现:
- 模块边界不清晰、改动影响范围越来越大
- 构建/测试变慢
- 多人协作冲突多
于是团队会自然想到:“拆开”,迎来阶段二。
注意:这里的 Monolith 是“一个应用越长越大”。它和后面的 Monorepo(多个包/项目同仓)不是同一个概念。
2.2 阶段二:多仓库多模块应用
把系统拆成多个仓库(例如:组件库仓库、业务 A 仓库、业务 B 仓库),会带来立竿见影的收益:
- 每个仓库更小、owner 更明确、权限更清晰
- 每个模块可以独立发版
- 单仓库的 CI 看起来更快(只跑自己的)
代码管理变得简化,构建效率也得以提升,这种代码管理方式称之为 MultiRepo。
但当仓库越来越多,新的成本也会越来越明显:
- 联动修改很难“原子化”:改组件库 API 后,你需要发布组件库,然后业务仓库分别升级、分别修、分别跑 CI。
- 版本同步链路变长:底层库升级,上层一堆仓库要跟着升级验证。
- 工程配置容易漂移:eslint/tsconfig/构建脚本在多个仓库逐渐不一致,治理难度上升。
这时候团队会意识到:拆仓库解决了局部自治,但放大了“协作与一致性”的成本。
2.3 阶段三:单仓库多模块应用
随着业务复杂度的提升,模块仓库越来越多,MultiRepo这种方式虽然从业务上解耦了,但增加了项目工程管理的难度,随着模块仓库达到一定数量级,会有几个问题:跨仓库代码难共享;分散在单仓库的模块依赖管理复杂(底层模块升级后,其他上层依赖需要及时更新,否则有问题);增加了构建耗时。于是将多个项目集成到一个仓库下,共享工程配置,同时又快捷地共享模块代码,成为趋势,这种代码管理方式称之为 Monorepo。
当“跨仓库联动成本”超过收益时,Monorepo 就变得有吸引力:
- 改公共包 + 改所有使用方,可以在一个 PR 一次性完成并验证
- 配置集中化,工程规范更易统一
- 公共能力更容易沉淀成 packages,减少复制粘贴和重复造轮子
当然,Monorepo 也不是没有代价:
- 仓库会变大(clone、索引、IDE 负担上升)
- 如果没有“按影响范围执行(affected)+ 缓存”,CI 可能会变慢)
3. Monorepo 优劣
![]()
| 场景 | MultiRepo | MonoRepo |
|---|---|---|
| 代码可见性 | ✅ 代码隔离,研发者只需关注自己负责的仓库 ❌ 包管理按照各自owner划分,当出现问题时,需要到依赖包中进行判断并解决。 |
✅ 一个仓库中多个相关项目,很容易看到整个代码库的变化趋势,更好的团队协作。 ❌ 增加了非owner改动代码的风险 |
| 依赖管理 | ❌ 多个仓库都有自己的 node_modules,存在依赖重复安装情况,占用磁盘内存大。 | ✅ 多项目代码都在一个仓库中,相同版本依赖提升到顶层只安装一次,节省磁盘内存, |
| 代码权限 | ✅ 各项目单独仓库,不会出现代码被误改的情况,单个项目出现问题不会影响其他项目。 | ❌ 多个项目代码都在一个仓库中,没有项目粒度的权限管控,一个项目出问题,可能影响所有项目。( |
| 开发迭代 | ✅ 仓库体积小,模块划分清晰,可维护性强。 ❌ 多仓库来回切换(编辑器及命令行),项目多的话效率很低。多仓库见存在依赖时,需要手动 npm link,操作繁琐。 ❌ 依赖管理不便,多个依赖可能在多个仓库中存在不同版本,重复安装,npm link 时不同项目的依赖会存在冲突。 |
✅ 多个项目都在一个仓库中,可看到相关项目全貌,编码非常方便。 ✅ 代码复用高,方便进行代码重构。 ❌ 多项目在一个仓库中,代码体积多大几个 G, git clone时间较长。 ✅ 依赖调试方便,依赖包迭代场景下,借助工具自动 npm link,直接使用最新版本依赖,简化了操作流程。 |
| 工程配置 | ❌ 各项目构建、打包、代码校验都各自维护,不一致时会导致代码差异或构建差异。 | ✅ 多项目在一个仓库,工程配置一致,代码质量标准及风格也很容易一致。 |
| 构建部署 | ❌ 多个项目间存在依赖,部署时需要手动到不同的仓库根据先后顺序去修改版本及进行部署,操作繁琐效率低。 | ✅ 构建性 Monorepo 工具可以配置依赖项目的构建优先级,可以实现一次命令完成所有的部署。 |
4. Monorepo 场景
场景一:大型项目与多项目协作
- 场景:企业或团队维护多个紧密关联的项目(如前端、后端、工具库等)。
- 优势:集中管理代码,方便跨项目修改和协作,避免代码分散导致的重复劳动。
场景二:共享代码与依赖
- 场景:多个项目共用组件库、工具函数或配置(如 UI 组件、通用 SDK)。
- 优势:直接引用内部模块,避免多仓库的版本同步问题,确保依赖一致性。
场景三:统一构建与持续集成(CI/CD)
- 场景:需要标准化构建、测试和部署流程。
- 优势:集中配置 CI/CD,仅针对变更部分触发构建(增量构建),提升效率。
何时谨慎使用?
- 代码量过大:需要考虑构建性能、代码可维护性
- 权限管理复杂:需细化目录权限控制
- 团队独立性高:若子团队高度自治,多仓库可能更灵活
5. Monorepo 工具
在采用 Monorepo(单一仓库)架构的软件开发中,工具的选择是至关重要的。合适的 Monorepo 工具能够帮助团队更高效地管理大规模代码库、提升协同开发体验以及优化构建和部署流程。
直至 2026 年年初,目前在前端界比较流行的 Monorepo 工具有 Pnpm Workspaces、Yarn Workspaces、npm Workspaces、Rush、Turborepo、Yalc、和 Nx
5.1 依赖管理工具
没有 workspace/工具链时:A 包要用 B 包,只能 npm link、复制代码、或走相对/绝对路径,非常别扭且容易错。
负责“怎么安装依赖、怎么把 workspace 包链接起来”
pnpm workspace 是包管理器层面的工作区能力:
- 支持 monorepo 内部包之间用“包名”互相依赖(不是路径引用),并自动链接到本地源码
- pnpm 有全局的内容存储(store),不同项目/不同 workspace 之间可以复用同版本依赖;再通过链接把依赖组织到各包的 node_modules 结构中。:直观效果:同一个依赖不需要在 N 个地方复制 N 份。
- 依赖安装更快、更省空间(全局 store 复用 + 链接)
- 默认依赖隔离更严格,可显著减少“幽灵依赖”
强烈推荐使用Pnpm Workspaces 作为 Monorepo 项目的依赖管理工具😍😍😍
- pnpm:通过全局 store + 链接方式,通常既省空间又更严格。
5.1.1 避免幽灵依赖
npm/yarn 安装依赖时,存在依赖提升,某个项目使用的依赖,并没有在其 package.json 中声明,也可以直接使用,这种现象称之为 “幽灵依赖”;随着项目迭代,这个依赖不再被其他项目使用,不再被安装,使用幽灵依赖的项目,会因为无法找到依赖而报错,而 pnpm 彻底解决这个问题
所谓幽灵依赖,可以理解为:
某个包没有在自己的 package.json 声明依赖,但因为安装结构/提升等原因,代码依然能 import 到它,直到某天依赖结构变化才突然报错。
pnpm 默认对依赖访问更严格,能更早暴露“未声明却在使用”的问题,让错误更早出现、定位更容易。
什么是幽灵依赖
先提问:你觉得“一个包能 import 某个依赖”的前提是什么?
正常答案应该是:
这个包的
package.json里 dependencies/devDependencies 声明了它。
幽灵依赖就是:没声明,但居然还能 import 并运行成功。
最小例子(用 npm/yarn 经典安装方式更容易出现):
假设是 monorepo:
- 根
package.json没有 lodash -
packages/a/package.json声明了lodash -
packages/b/package.json没声明lodash
但在 packages/b/src/index.ts 里写了:
import _ from "lodash";
在 npm/yarn(node_modules 提升/hoist) 的某些安装结果下,lodash 可能被“提升”到了更上层的 node_modules,导致 b 虽然没声明,也能“碰巧”找到 lodash,于是:
- 开发阶段:你以为没问题
- 某天 a 删除了 lodash 或版本变化/安装结构变化:b 突然就挂了
这就像:你家隔壁有个锤子,你没买但你一直去借用;直到隔壁搬家,你才发现自己其实从来没拥有它。
为什么 pnpm 更容易避免?
pnpm 的默认策略更“严格”:
- 每个 package 能访问到的依赖,基本只限于它声明的那一圈(通过链接+隔离结构实现)
- 所以 b 没声明 lodash,就更容易直接报错(这反而是好事:早发现早修)
一句话总结你可以写进文章:
幽灵依赖:未在当前包的 package.json 声明,却因为依赖提升等原因在运行时能被解析到的依赖;pnpm 通过更严格的依赖隔离,能显著减少这类问题。
5.1.2 依赖安装耗时长
MonoRepo 中每个项目都有自己的 package.json 依赖列表,随着 MonoRepo 中依赖总数的增长,每次 install 时,耗时会较长。使用 pnpm 按需安装及依赖缓存,相同版本依赖提升到 Monorepo 根目录下,减少冗余依赖安装;
那么 Monorepo 与包管理工具(npm、yarn、pnpm)之间是一种怎样的关系?
这些包管理工具与 monorepo 的关系在于,它们可以为 monorepo 提供依赖安装与依赖管理的支持,借助自身对 workspace 的支持,允许在 monorepo 中的不同子项目之间共享依赖项,并提供一种管理这些共享依赖项的方式,这可以简化依赖项管理和构建过程,并提高开发效率。
5.1.3 构建打包耗时长
问题:多个项目构建任务存在依赖时,往往是串行构建 或 全量构建,导致构建时间较长,可以使用增量构建,而非全量构建;也可以将串行构建,优化成并行构建。
npm、yarn、pnpm 等是用来管理项目依赖、发布包、安装依赖的工具,他们都提供了对工作区(workspace)的支持,允许在单个代码库中管理多个项目或包。这种工作区支持在单个代码库中同时开发、测试和管理多个的项目,而无需使用多个独立的代码仓库。
这些包管理工具与 monorepo 的关系在于他们可以为 monorepo 提供依赖安装与依赖管理的支持,借助自身对workspace的支持,允许在monorepo中的不同子项目之间共享依赖项,并提供一种管理这些共享以来想的方式,这可以简化依赖项管理和构建过程,并提高开发效率。
硬链接指向同一份文件数据,因此可以复用磁盘空间。
5.2 任务编排/构建系统
负责“有哪些任务要跑、哪些可以并行、哪些可以跳过、结果怎么缓存复用(增量构建)”
没有任务编排/增量构建时:一个仓库多个包,但 CI/构建经常只能全量跑,慢;发布也麻烦。
Nx/Turborepo/Rush
用一个场景立刻区分:只改了 UI 组件库,会发生什么?
假设 monorepo 里有:
-
packages/ui(组件库) -
apps/web(业务) apps/admin
改了 packages/ui/Button.tsx
-
pnpm workspace 会做什么?
让apps/web依赖的@repo/ui指向本仓库的 ui 源码(链接),并保证依赖安装正确、边界更严格。 -
turbo/nx 会做什么?
计算“受影响范围”:ui变了 ⇒web/admin可能都受影响
然后只跑:ui build+web build+admin build(而不是全仓库所有包都 build)
并且能并行、能缓存。
6. 总结
- Monorepo 并不是银弹,而是一种权衡工程管理与项目协作复杂性的最佳实践之一。适用于项目关联紧密、需频繁联动、强调一致性的中大型团队/企业。
- 通过引入现代的包管理工具(如 pnpm workspace)和任务编排系统(如 Turborepo、Nx),Monorepo 管理的优势可以最大化,同时减轻依赖和构建上的压力。
- 采用 Monorepo 可以促进团队协作、统一规范和复用代码,但也需留意仓库增大、权限细化等实际挑战。
- 是否采纳 Monorepo,需结合企业项目规模、团队协作方式、基础设施支持等多方面因素综合考量。
- 总之,合理组合工具和规范,才能真正发挥 Monorepo 的价值,为团队降本增效。