阅读视图

发现新文章,点击刷新页面。

Monorepo入门

1. Monorepo 介绍

核心价值:把“需要一起演进的一组项目”放在同一个版本空间里,从而让跨项目改动(API 变更、重构、升级)能在一次提交里完成并验证

Monorepo 是把多个相关项目/包放在同一个 Git 仓库中管理的策略,有助于跨项目联动修改、内部包共享更顺畅、统一规范与 CI、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。

Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。

2. Monorepo 演进

image.png

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 优劣

image.png

场景 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 WorkspacesYarn Workspacesnpm WorkspacesRushTurborepoYalc、和 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 的价值,为团队降本增效。
❌