Monorepo 是什么?它适合解决什么问题?
简化版
Monorepo 是在一个版本库中管理多个应用和包,并不等于把它们发布成同一个版本。它适合共享代码多、跨包改动频繁、需要统一工具链的团队;收益依赖 workspace、依赖边界、受影响任务、缓存和发布治理,否则只会把多个项目堆进一个大仓库。
详细版
Workspace 负责本地包发现、链接和依赖安装;Turborepo、Nx 等任务系统负责依赖图、任务编排、受影响计算和缓存;Changesets 等工具可管理可发布包的版本与变更说明。三者职责不能混为一谈。
Monorepo 的优势是跨包修改可在同一个 PR 中原子提交、规范统一、复用方便。代价是仓库体积、CI 调度、权限隔离和错误依赖扩散都更复杂。
是否采用要看协作关系,而不是包数量。共享很少、强权限隔离、发布完全独立的项目,Multirepo 往往更简单。
完整版教学
一、Monorepo 改变的是协作与版本库边界
单仓并不要求单应用,也不要求所有包同版本。一个仓库可以同时放 Web、管理台、组件库、工具包和配置包,每个包仍可独立构建与发布。
repo/
├─ apps/web
├─ apps/admin
├─ packages/ui
├─ packages/utils
└─ packages/eslint-config
记忆钩子:Monorepo 是“同一版本库”,workspace 是“依赖连接器”,任务系统是“调度器”,发布工具是“版本管理员”。
二、它怎样降低跨包修改成本
Multirepo 中修改组件 API,常要先发布组件测试版本,再更新多个应用并协调合入顺序。Monorepo 可以在同一分支同时改组件、调用方和测试,CI 对整个变更集合验证。
若一次重构涉及 1 个组件库和 4 个应用,Multirepo 至少跨 5 个 PR 协调;单仓可用 1 个 PR 表达原子变化。PR 数量不是唯一成本,但能直观看出接口演进和回滚更集中。
统一配置也能减少漂移,例如共享 TypeScript、ESLint 和测试预设。不过统一不等于所有项目必须完全相同,差异应通过清晰扩展点表达。
三、workspace 解决安装,不自动解决构建
workspace 会识别包并把内部依赖链接到本地,通常还能集中安装外部依赖。它不会自动知道 build 应先跑哪个包,也不会凭空提供远程缓存或发布策略。
| 能力 | workspace | 任务系统 | 发布工具 |
|---|---|---|---|
| 本地包链接 | 是 | 否 | 否 |
| 任务依赖图 | 基础或无 | 核心能力 | 否 |
| 构建缓存 | 通常不是核心 | 核心能力 | 否 |
| 版本与 changelog | 否 | 否 | 核心能力 |
内部依赖应在各包清单中显式声明。依靠根目录“碰巧安装”的幽灵依赖会让本地可运行、独立发布后却缺包。
四、受影响任务与缓存为什么决定可扩展性
最朴素 CI 会对每次提交构建所有项目,仓库越大越慢。任务系统根据文件变化和包依赖图计算受影响集合:修改 ui 时,构建 ui 及其下游应用;只改文档时可能无需重跑全部任务。
假设 40 个包每个测试平均 2 分钟,完全串行需 80 分钟;一次改动只影响 6 个包,受影响执行是 12 分钟,再配合并行和缓存还能缩短。前提是依赖图准确,隐藏的跨包读取会让缓存命中却产出错误结果。
缓存键必须包含源码、依赖、命令、环境和工具版本等有效输入。远程缓存还要考虑访问权限,因为产物或日志可能包含私有代码信息。
五、包边界和依赖规则如何防止“大泥球”
所有代码近在同仓,开发者很容易跨目录深层引用,逐渐破坏模块边界。应通过公开入口、路径约束和 lint 规则限制“应用依赖包、基础包不反向依赖应用”等方向。
apps ───────> feature packages ───────> shared packages
允许向右依赖,不允许 shared 反向引用 apps
循环依赖会让构建顺序、初始化和发布变得不稳定。架构规则应进入 CI,而不是只画一张团队没人维护的分层图。
权限也是边界:单仓默认扩大可见面,CODEOWNERS 只能控制审核,未必等同于强读取隔离。受监管或强保密代码可能更适合独立仓库。
六、版本发布有统一与独立两种策略
固定版本策略让所有包一起升版,概念简单但会产生无实际变化的版本;独立版本策略只升级受影响包,更精确,却需要维护包间兼容范围和发布顺序。
组件库发布还要处理 breaking change、预发布、changelog 和失败恢复。应用不发布到 npm,也仍需版本化部署制品,不能把“包发布”和“应用部署”混成一件事。
| 场景 | 更合适策略 | 原因 |
|---|---|---|
| 强耦合框架套件 | 统一版本 | 兼容关系简单 |
| 多个独立工具包 | 独立版本 | 减少无关升级 |
| 纯内部应用 | 制品版本 | 不一定需要包版本 |
迁移应从依赖盘点和边界治理开始,再引入任务缓存。先把仓库合并、后补规则,短期看似成功,长期会把隐式耦合放大。
七、常见误区与追问
- 误区:Monorepo 中所有包必须同版本发布。 单仓与版本策略是两个独立决策,可以统一也可以独立。
- 误区:使用 pnpm workspace 就自动拥有受影响构建。 workspace 主要管理包与依赖,复杂任务调度需要额外能力。
- 误区:代码放在一起就自然实现复用。 没有稳定包边界,所谓复用会退化成任意跨目录引用。
- 追问:怎样避免每次 CI 构建全仓? 基于依赖图计算受影响任务,并用正确输入建立本地或远程缓存。
- 追问:Monorepo 如何做权限控制? 可用代码所有者和目录规则做审核;需要强读取隔离时应评估拆仓。
- 追问:幽灵依赖是什么? 包使用了未在自身清单声明、只因根安装布局而偶然可见的依赖。
- 追问:什么团队不适合 Monorepo? 共享少、边界强、权限隔离严格且缺少工程平台能力的团队。
八、加强记忆
用“同仓、显依赖、算影响、守边界、管发布”理解 Monorepo:单仓让跨包变更原子化,workspace 让内部依赖可解析,任务图只跑受影响工作,规则阻止仓库变成大泥球,版本工具处理独立或统一发布。是否采用最终取决于协作收益能否覆盖平台治理成本。