深入理解 Monorepo:概念梳理

59 阅读9分钟

新技术的的诞生一定是有它的技术背景的。新技术一定是适合公司、团队的技术吗?我看这倒未必。鞋合不合适只有脚知道。但是不断的了解和学习新知识、新技术应该是每一个程序员的安身立命之本,尤其对于发展极快的前端领域来说,更是如此,可以不用,但不能不知道、不能不了解。本文将详细的对 Monorepo 工程化技术进行讲解,第二篇文章将会进行工程落地应用。帮助大家后续对 Monorepo 的选型和应用做一个参考。

随着前端工程化的演进,前端项目早已不是"一个仓库、一个应用"的简单形态:一个业务体系往往包含多个 Web 应用、多个 Node 服务、若干共享组件库和工具函数库。这些项目如何组织?是各自建仓(Multi-repo),还是塞进同一个仓库(Monorepo)?

Monorepo 作为 Google、Meta、微软等大厂多年实践验证过的策略,近几年凭借 pnpmTurborepoNx 等工具链的成熟,在国内前端社区也全面流行起来。本文将从概念、核心对比、核心机制、挑战与代价、工具生态五个维度,系统性地讲透 Monorepo。

工程落地文章地址:深入理解 Monorepo:工程落地在上一篇中对 Monorepo 的基本概念进行了讲解,在本篇文章中,将会结合项目代码 - 掘金

子包安装依赖文章地址:深入理解 Monorepo:子包安装依赖前言 当你从传统的单仓单包项目迁移到 Monorepo 后,第一个遇到的问题往往 - 掘金


一、什么是 Monorepo

1.1 定义

Monorepo(Mono + Repository) 是一种代码组织策略:在一个版本控制仓库(如一个 Git 仓库)中,管理多个彼此独立、但又相互依赖的项目或包。通过下面这张图可以清晰的看到 Monorepo 和我们以前经常使用的 Multi-repo 多仓库的区别。

Snipaste_2026-09-01_15-35-00.png

一个 Git 仓库
├── 应用 A(前端 Web)
├── 应用 B(Node API 服务)
├── 组件库(UI 库)
├── 工具库(utils / hooks)
└── 配置包(lint / tsconfig 统一配置)

需要澄清一个常见的误区:Monorepo ≠ 把所有代码堆进一个大文件夹

它不是"没有边界的大杂烩",恰恰相反,Monorepo 的核心是在单一仓库内保持清晰的模块边界:每个包有自己的 package.json、自己的职责、自己的依赖声明,只是它们共享同一个仓库、同一套工具链和同一次提交。

与之相对的是 Multi-repo / Polyrepo(多仓库):每个项目各自独立建仓,项目之间通过"发布 npm 包 → 安装"的方式协作。

1.2 大厂实践(已被验证的先行者)

公司仓库规模核心工具
Google全球最大的 Monorepo,代码量达数十亿行、覆盖几乎所有语言自研 Bazel + Piper
Meta大型 Monorepo,支撑 Facebook/Instagram 等自研 Buck
Microsoft大型 TypeScript / JS 工程Rush(配合 pnpm 存储)
国内大厂(字节、腾讯等)前端统一多仓/单仓混用,逐步向 Monorepo 演进pnpm + Turborepo / Nx

二、Monorepo vs Multi-repo:核心对比

选择哪种策略,本质上是在协作效率隔离自治之间做权衡。

Multi-repo(传统多仓库)

repo-app1/     repo-app2/     repo-utils/     repo-ui/
├── src/       ├── src/       ├── src/        ├── src/
├── package    ├── package    ├── package     ├── package
└── .git       └── .git       └── .git        └── .git

问题:
- 依赖管理困难
- 代码复用麻烦
- 跨项目修改需要多次提交
- 版本不一致

Monorepo(单体仓库)

monorepo/
├── packages/
│   ├── app-web/          # Web 应用
│   ├── app-admin/        # 后台管理
│   ├── ui-components/    # UI 组件库
│   ├── utils/            # 工具函数
│   └── hooks/            # Vue Hooks
├── package.json
└── .git                  # 只有一个 .git

优势:
- 统一管理
- 代码共享方便
- 原子化提交
- 依赖一致性

2.1 对比总览

维度Monorepo(单仓库)Multi-repo(多仓库)
代码共享✅ 直接引用、实时同步,无需发布-安装❌ 需要发布 npm 包 → 各项目分别安装升级
依赖管理✅ 单一 lockfile,统一版本,避免碎片化❌ 各仓独立 lockfile,版本容易漂移
原子变更✅ 跨项目改动一次提交、一起测试、一起发布❌ 需跨仓开多个 PR,手动协调发布顺序
重构效率✅ 原子性重构(如重命名 API 全仓库同步)❌ 跨仓库重构需要发版+逐个升级
构建/测试✅ 增量构建 + 缓存 + 影响范围分析❌ 重复构建、无法共享缓存
权限/隔离❌ 默认全员可见,需 CODEOWNERS 等机制✅ 天然按仓库隔离
团队自治❌ 工具链/规范需统一✅ 各团队技术栈相对自由
Git 性能❌ 仓库增大后 clone/checkout 变慢✅ 仓库小、操作快
工具链复杂度❌ 需要专门的构建编排工具✅ 简单直接

2.2 结构对比示意图

Monorepo 博客介绍 (4).jpeg

2.3 什么时候 Monorepo 明显占优

  • 多个应用/服务共享大量代码(组件、工具、类型、配置);
  • 经常需要跨项目协同改动(改一个 API 影响多个调用方);
  • 希望统一技术栈、规范与发布节奏
  • 想省去"内部包发布 → 安装 → 升级"的低效循环。

2.4 为什么选择 Monorepo

2.4.1 代码复用

// 任何项目都可以直接引用共享包
import { formatDate } from '@my-monorepo/utils';
import { Button } from '@my-monorepo/ui';

2.4.2 原子化提交

# 一次提交同时修改多个包
git commit -m "feat: 更新用户模块"

# 包含:
# - packages/app-web/src/user.ts
# - packages/utils/src/user.ts
# 确保代码同步更新

2.4.3. 统一的依赖管理

{
  "dependencies": {
    "vue": "^3.3.0"  // 所有项目使用同一版本
  }
}

2.4.4. 简化协作

团队 A 修改了 utils → 团队 B 立即看到变化
无需发布 npm 包,无需等待版本更新

三、Monorepo 的核心机制:它是怎么工作的

要真正理解 Monorepo,需要掌握四个关键机制。

3.1 Workspace(工作区):依赖的"虚拟合并"

Workspace 是 Monorepo 的基石。在仓库根目录声明 pnpm-workspace.yaml / package.jsonworkspaces 字段),把 packages/*apps/* 下的所有包纳入一个"工作区",由包管理器统一解析、统一安装依赖、统一生成一个 lockfile

Monorepo 博客介绍 (6).jpeg

3.2 本地包互相引用:不走 npm registry

Monorepo 中最关键的一点:包之间的引用不依赖发布。以 pnpm 为例:

  • 包 A 依赖包 B,只需在 A 的 package.json 里写 "b": "workspace:*"
  • pnpm 会在 node_modules 中建立指向本地 B 的链接,B 的改动即时生效,无需发版;
  • workspace:* 协议在发布时自动替换为实际版本号。

Monorepo 博客介绍 (8).jpeg

3.3 任务编排(Task Pipeline):按依赖图调度

多个包之间有构建先后依赖(如 ui 要先构建,web 才能构建)。专门的任务编排工具(Turborepo / Nx / Rush)会在 turbo.jsonnx.json 中声明任务依赖,构建时按 DAG(有向无环图) 并行调度:

Monorepo 博客介绍 (7).jpeg

3.4 增量缓存 + 影响范围分析(Affected)

这是 Monorepo 控制构建成本的两大杀手锏:

  • 缓存:相同输入(源码 + 配置 + 依赖哈希)的任务直接命中缓存,本地/远程共享,秒级跳过;
  • 影响范围分析:根据"本次变更了哪些文件"反推出"哪些包受影响",只构建、只测试受影响的部分Monorepo 博客介绍 (5).jpeg

注:Nx 的 affected 基于 import 依赖图(更精确),Turborepo 基于文件哈希。


四、Monorepo 的挑战与代价(别只看到好处)

任何架构决策都有代价。Monorepo 的主要痛点:

  1. 仓库体积与 Git 性能:代码量变大后,clone、checkout、blame 变慢,需配合浅克隆、Git LFS、稀疏检出等方案;
  2. 构建/测试放大:若没有缓存与影响范围分析,全量 CI 会非常慢——工具链配置不到位,Monorepo 就是灾难
  3. 权限粒度变粗:默认全仓库可见,需要 CODEOWNERS、分支保护、评审规则来弥补;
  4. 工具链复杂、学习成本高:Turborepo / Nx / Rush 的 pipeline、缓存、发布策略都需要团队统一认知;
  5. 过度耦合风险:如果服务边界不清晰,容易产生隐式依赖,破坏模块化。

应对思路:小步引入、重视工具链投入、用目录/依赖检查(如 eslint-plugin-boundaries)守住边界。


五、主流工具生态:分层看,别混为一谈

很多人把"Monorepo 工具"当成一个东西,其实生态是分层的。理解分层才能正确选型。

5.1 生态分层

层次职责代表工具
包管理器层声明 workspace、安装依赖、锁文件、依赖链接npm、Yarn Berry、pnpm
版本/发布层多包版本号管理、changelog、发布Changesets、Lerna
任务编排/构建层任务管道、缓存、影响范围分析TurborepoNx、Rush、Moon、Bazel

5.2 包管理器对比

特性npm workspacesYarn Berrypnpm workspaces
磁盘空间⭐⭐⭐⭐⭐⭐⭐⭐⭐(内容寻址存储 + 硬链接)
依赖隔离⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐(虚拟隔离目录)
幽灵依赖问题❌ 有部分✅ 基本杜绝
生态成熟度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
安装速度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

结论:当前 JS 社区 Monorepo 的事实标准是 pnpm workspaces,尤其是它的磁盘占用、依赖隔离和 workspace:* 协议。

5.3 任务编排/构建工具对比

能力TurborepoNxLernaRushMoon
底层实现RustTypeScriptJSTypeScriptRust
配置复杂度中-高
增量构建/缓存✅ 本地+远程✅ 本地+Nx Cloud✅ 自建远程
影响范围分析✅ 文件哈希✅ 基于依赖图(更精确)有限
代码生成器✅ 完整生成器体系
模块边界约束手动✅ ESLint 规则强制
框架集成通用深度集成 Next/React 等通用通用通用
适用场景中型 JS/TS 工程、极致构建速度复杂企业级、多团队、需精确 affected纯 npm 多包发布超大型 TS 仓库追求速度的新项目

补充几点关键背景:

  • Lerna:老牌工具、GitHub Stars 长期领先,专注多包发布;维护节奏一度放缓,后并入 Nx 生态,现阶段更适合"纯发布"场景;
  • Turborepo:Vercel 出品,配置极简,用哈希缓存 + 远程缓存做任务编排,是"最快上手"的构建层方案;
  • Nx:Nrwl 打造的全功能平台,提供项目图(Project Graph)、生成器、模块边界约束、AI 修复等,功能最完整但学习曲线陡;
  • Rush:微软设计,面向超大规模 TS 仓库,配置重但能力强;
  • Bazel:Google 出品,多语言、可复现(hermetic)构建,适合巨型混合语言仓库,学习成本极高,前端一般用不上。