Monorepo 工程化完整方案:从目录边界到 CI 加速的落地 Playbook

HTMLPAGE 团队
18 分钟阅读

给出一套可复用的 Monorepo 工程化方案:什么时候该用 monorepo、如何划分包边界、如何用 pnpm workspace 管理依赖、如何把 lint/typecheck/build/test 变成“可按影响范围执行”的 CI 门禁,以及常见坑的排障清单。

#monorepo #pnpm #工程化 #CI #代码规范 #依赖治理

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 条底线

  1. 禁止跨目录相对引用(例如 ../../packages/ui/src/...
  2. 共享代码必须通过包发布/依赖(workspace 包也算“发布”)
  3. 每个包有明确的 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 依赖治理:把“版本冲突”变成可管理的事情

建议建立仓库级策略:

  • 常用基础依赖统一版本(例如 vuereacttypescript
  • 使用 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/构建越来越慢。

治理:

  • .outputdist.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)。