Monorepo 工程化完整方案:从目录边界到 CI 加速的落地 Playbook
“要不要上 monorepo?”这不是一个技术潮流问题,而是一个组织协作与工程成本问题。
如果你只是把多个项目塞进同一个仓库,但:
- 依赖边界混乱
- CI 仍然全量跑
- 本地启动越来越慢
- 版本冲突/重复依赖越来越多
那你得到的往往不是效率,而是更大的复杂度。
本文给你一套可落地的 monorepo Playbook:先建立边界,再建立工具链,再建立 CI 的“按影响范围执行”,最后把规则固化为流程。
1. 什么时候 monorepo 值得做(以及不值得做的情况)
值得做的典型信号
- 你维护多个应用/站点,它们共享 UI、工具库、业务模块或配置
- 你经常遇到“A 项目改了一个公共包,B 项目同步很痛苦”
- 你希望统一代码规范、统一构建与发布流程
- 你有明确的工程目标:减少重复、加速 CI、提高一致性
不值得做(或要谨慎)的情况
- 团队很小,只有一个应用,且短期不会拆分
- 你的项目高度独立,几乎不共享代码
- 你没有能力/意愿为“边界与工具链”持续投入
一句话总结:
monorepo 的收益来自“共享与一致性”,成本来自“边界治理”。
2. 目录结构:先把“边界”做成硬约束
一个好的 monorepo 目录结构应该做到:
- 应用与包清晰隔离(避免互相污染)
- 每个子项目可以独立启动/构建
- 共享能力通过“包依赖”传递,而不是复制粘贴
常见布局(示意):
repo/
apps/
web/
admin/
packages/
ui/
utils/
config/
docs/
package.json
pnpm-workspace.yaml
边界治理的 3 条底线
- 禁止跨目录相对引用(例如
../../packages/ui/src/...) - 共享代码必须通过包发布/依赖(workspace 包也算“发布”)
- 每个包有明确的 public API(不要让 consumer 依赖内部文件结构)
如果你做不到这三条,monorepo 很难长期稳定。
3. 依赖管理:用 pnpm workspace 把“共享”变得可控
3.1 pnpm-workspace.yaml:定义 workspace 边界
示例:
packages:
- "apps/*"
- "packages/*"
这一步的意义是:
- 让 workspace 知道哪些目录属于“可联动安装”的包
- 让依赖提升/链接策略在整个仓库内统一
3.2 workspace: 协议:让内部依赖显式可追踪
在 app 中依赖内部包时,优先使用:
{
"dependencies": {
"@acme/ui": "workspace:*"
}
}
好处是:
- 依赖关系在 lockfile 中清晰
- 本地开发天然走软链接/工作区链接
- 不会误用外部同名包
3.3 依赖治理:把“版本冲突”变成可管理的事情
建议建立仓库级策略:
- 常用基础依赖统一版本(例如
vue、react、typescript) - 使用
pnpm.overrides统一修复漏洞或锁定版本
示例(根 package.json):
{
"pnpm": {
"overrides": {
"lodash": "4.17.21"
}
}
}
4. 工具链统一:把“脚本矩阵”收敛为一组标准动作
monorepo 常见的失败点是:脚本太多、每个包叫法不一样,CI 难以组合。
建议你在每个包里尽量对齐以下脚本语义:
lint:只检查(CI 用)lint:fix:本地修复typecheck:类型检查test:单测build:构建
根目录再提供“编排脚本”,例如:
{
"scripts": {
"lint": "pnpm -r lint",
"typecheck": "pnpm -r typecheck",
"build": "pnpm -r build"
}
}
其中 -r 表示递归运行所有 workspace 包。
5. 关键能力:让 CI 只跑“受影响的包”(而不是全量)
monorepo 的核心收益之一是:能把 CI 从全量变成增量。
5.1 先定义“影响范围”
一个 PR 修改了哪些内容,会影响哪些工作?
- 改了
packages/ui:可能影响所有 consumer apps - 改了某个 app 的页面:只影响该 app
- 改了共享配置:可能影响全仓
你需要让工具链能回答:
这次改动,哪些包需要重新 lint/typecheck/build/test?
5.2 pnpm 的 filter:最简单可用的增量方式
你可以用 --filter 精准运行某个包:
pnpm --filter web build
也可以运行“依赖它的包”(反向依赖),常见做法:
- 先确定改动的包
- 再对受影响的 app 执行 build/test
如果你的 monorepo 规模更大,可以引入任务编排与缓存(例如 Turborepo/Nx),但那是“第二阶段”。
6. 发布与版本:把“变更传播”做成流程
monorepo 的版本策略主要有两类:
6.1 单版本(single version)
- 整个仓库一个版本号
- 发布简单,但粒度粗
适合:应用为主、包为辅的仓库。
6.2 多版本(independent version)
- 每个包独立版本
- 需要变更记录与发布流程(例如 changesets)
适合:包作为核心产品、被多个仓库/团队消费。
无论哪种策略,关键是:
- 变更要有记录(changelog)
- 发布要可重复(CI 发布)
- 依赖升级要可追踪(PR 驱动)
7. 常见坑与排障清单(把问题前置)
7.1 幽灵依赖(phantom dependencies)
表现:代码能跑,但换台机器或 CI 就失败。
原因:某个包“偷用了”另一个包的依赖(没有写进自己的 package.json)。
治理:
- 严格 lint import
- 避免过度 hoist
- 让 CI 必须从干净环境安装并运行
7.2 循环依赖
表现:构建顺序混乱、运行时异常、类型解析奇怪。
治理:
- 依赖图可视化
- 从架构上打断环(提取公共层、调整依赖方向)
7.3 产物目录被扫描导致变慢
表现:watch/构建越来越慢。
治理:
- 把
.output、dist、.nuxt等目录纳入 ignore - 避免把生成物提交进仓库
8. 一份可直接执行的落地路线图(建议照顺序做)
第 1 周:建立边界 + 统一脚本语义
- 明确 apps/packages 结构
- 禁止跨目录相对引用
- 对齐
lint/typecheck/build/test
第 2 周:把 CI 从全量变成“按包运行”
- 用
pnpm -r先跑通全量 - 再引入
--filter按包执行 - 为关键 app 建立最小门禁
第 3-4 周:做依赖治理与性能治理
- 统一核心依赖版本
- 增加缓存策略
- 针对最慢的 20% 任务做专项优化
结语:monorepo 的本质是“用边界换协作效率”
monorepo 的关键不是目录长什么样,而是:
- 边界是否清晰
- 规则是否能落地(工具链 + CI)
- 是否能用流程抵抗长期演进的熵增
如果你愿意,我可以按你们仓库现状再给一份“针对本仓库的增量 CI 拆解方案”(哪些脚本该下沉到 package、哪些该保留在根目录、哪些目录必须 ignore)。


