认识 Monorepo:为什么多个包要放进一个仓库

本节目标

先从一个真实痛点说起。假设你有 3 个前端包:web(官网)、admin(后台)、ui(组件库)。如果用 Multirepo(多个独立仓库),会出现三件糟心事:

  1. 依赖版本地狱:web 装了 lodash@4.17.20,admin 装了 4.17.21,同一个 bug 在两个仓库表现不一样,排查到怀疑人生。
  2. 改一处要发三个 PR:ui 组件库改了个 Button 的 props,你得先在 ui 发版,再去 web、admin 各提一个「升级 ui 版本」的 PR,等它们各自 CI 跑完——一整天就这么没了。
  3. 类型/工具无法共享:公共 TS 类型、ESLint 配置、构建脚本,每个仓库复制一份,改规则时要同步 N 个仓库。

Monorepo 就是反过来:把「多个相关包/应用」放在「同一个仓库」里统一管理。上面的问题迎刃而解——改 ui 的 Button,在同一个仓库里顺手改 web/admin 的调用,一次 commit 全搞定;依赖只有一份,版本天然统一。

// Monorepo 的目录长这样(一个仓库,多个包)
// my-monorepo/
// ├─ package.json          // 根:管理 workspace
// ├─ pnpm-workspace.yaml   // 声明哪些目录是「包」
// └─ packages/
//    ├─ web/              // 应用 A
//    ├─ admin/            // 应用 B
//    └─ ui/              // 共享组件库

但要注意:Monorepo 不是银弹。它带来一个代价——「仓库很大、CI 很慢、工具链更复杂」。小团队只有一个包时,上 Monorepo 纯属自找麻烦。我的判断线很清晰:当你的代码需要「跨包复用 + 频繁联动修改」时,才值得上 Monorepo;否则一个普通仓库 + 一个 npm 私有包就够了。

名词解释

Monorepo(单体仓库):把多个相关项目放在「同一个 Git 仓库」里管理,共享同一套依赖、配置与 CI。注意它和「巨石应用(Monolith)」不同——仓库是一个,但里面仍然是多个可独立构建/发布的包。

Multirepo(多仓库):每个项目一个独立 Git 仓库,各自有独立的依赖、版本与发版流程。隔离性好,但跨项目改动成本高。

Workspace(工作区):包管理器(pnpm/npm/yarn)提供的「把仓库内多个子包当成一个整体来安装、链接」的能力。它是 Monorepo 的底层机制。

幽灵依赖(Phantom Dependency):本该只在 A 包 dependencies 里的包,因为被 B 包提升到了仓库根 node_modules,导致 C 包没声明也能 import 到它。一旦 A 改了依赖,C 就莫名其妙崩了。Monorepo 工具优劣很大程度取决于能不能杜绝它。

课后练习

练习 1:团队只有一个 web 应用、没有共享组件库,要不要上 Monorepo?

答案:不要。Monorepo 的价值来自「多包复用与联动」,单包场景下它只会增加工具链复杂度,用一个普通仓库足矣。

练习 2:Monorepo 和「把所有代码塞进一个巨石应用」是一回事吗?

答案:不是。Monorepo 是「一个仓库、多个独立包」,包之间仍边界清晰、可单独构建发布;巨石应用是「一个仓库、一个打包产物」,所有代码耦合在一起。前者治的是「协作与复用」,后者往往是技术债。

总结

我的立场很明确:Monorepo 是「协作规模」的解药,不是「项目大小」的解药。它用「统一仓库 + 统一依赖 + 原子化改动」换来了跨包联动的丝滑,代价是工具链与 CI 的复杂度。判断要不要上,只看一件事——你的包之间是否「经常要一起改、一起复用」。是,就上;否,别凑热闹。下一节我们直接动手,用 pnpm workspace 从零搭一个能跑的 Monorepo。