深入理解 Monorepo:工程落地

31 阅读4分钟

在上一篇中对 Monorepo 的基本概念进行了讲解,在本篇文章中,将会结合项目代码,对 Monorepo 的工程落地进行一个应用。大家可以结合实际代码在本地运行一下整个项目,对项目的目录结构、运行方式、函数、模块如何调用,以及在 Monorepo 仓库中的各个子包是如何关联调用起来的。

上一篇的概念梳理文章地址:深入理解 Monorepo:概念梳理新技术的的诞生一定是有它的技术背景的。新技术一定是适合公司、团队的技术吗?我看这倒未 - 掘金

GitHub 地址:wangyang-tpri/MyMonorepo: monorepo 单体化仓库

从0到1完整搭建一个可用的 monorepo 单体化仓库,并将工程治理工具(ESLint+Prettier+dependency-cruiser+CODEOWNERS+manypkg)全部配置到项目中了。

项目本地运行成功截图:可以看到 Monorepo 项目中的 web 应用分别使用了 ui-components 和 utils。

Monorepo运行成功截图.png 注意: 使用 pnpm + workspace 创建的 Monorepo 项目,不管是根目录下的依赖,还是子包中的依赖都不能使用 yarn 和 npm 安装和管理的,必须统一使用 pnpm 进行管理。

一、目录结构设计

1.1 常见布局

my-monorepo/
├── apps/                       # 可部署的应用
│   ├── web/                    # 前端web应用
├── packages/                   # 可复用共享库
│   ├── ui-components/          # 组件库 @scope/ui
│   ├── utils/                  # 工具函数 @scope/utils
│   ├── hooks/                  # Vue Hooks @scope/hooks
│   └── config/                 # 统一配置 @scope/config(eslint/构建)
├── tools/                      # 工程脚本/CLI
├── .github/                    # CI 工作流
├── pnpm-workspace.yaml         # 工作区声明
├── turbo.json                  # 任务编排(可选)
├── package.json                # 根 package.json
├── pnpm-lock.yaml              # 统一锁文件
└── README.md

1.2 命名与规范要点

  • 包名统一使用 @scope/包名,包目录名与包名保持一致(如 packages/ui@scope/ui);
  • 根目录只放公共配置与脚本,不放业务代码;
  • 公共配置(eslint、prettier、tsconfig、构建预设)抽到 packages/config 或根配置,避免重复;
  • package.jsonprivate: true 区分"可发布包"与"纯内部包"。

二、工程实践:从零搭建一套 Monorepo

2.1 第一步:pnpm + workspace 起步

# 初始化
npm i -g pnpm
mkdir my-monorepo && cd my-monorepo
pnpm init

创建 pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"

创建三个测试包,apps/web 依赖 packages/ui-components 依赖 packages/utils

依赖关系为:utils -> ui-components -> app-web
// packages/ui-components(节选)
{
  "name": "@my-monorepo/ui",
  "version": "0.0.0",
  "dependencies": {
    "@my-monorepo/utils": "workspace:*"
  }
}

安装并验证软链:

pnpm install

此时 packages/ui-components 下的 @my-monorepo/utils 会直接链接到本地 @my-monorepo/utils,改动即时生效。apps/app-web 也是同样的道理,会直接链接到本地的 @my-monorepo/uitls@my-monorepo/ui

2.2 第二步:接入 Turborepo 做任务编排

pnpm add -D turbo

根目录 turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.js", "src/**/*.vue", "test/**/*.js"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

package.json scripts:

{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  }
}

此后 pnpm build 会按依赖图并行、缓存化地构建所有包。

turbo缓存构建整个项目.png

2.4 第五步:统一代码质量与边界

  • 根目录统一 ESLint + Prettier 配置;
  • eslint-plugin-boundariesdependency-cruiser(依赖关系) 检查包间依赖是否越界;
  • CODEOWNERS(代码评审) 管理关键目录的评审负责人;
  • 引入 syncpack / manypkg(依赖版本) 检查跨包依赖版本是否一致。
  • 根目录管 全局和调度, 子包管 自身和执行

三、什么时候不该用 Monorepo

Monorepo 不是银弹,以下情况请慎重:

  1. 团队很小、项目彼此独立:共享代码很少,合并后徒增工具链成本;
  2. 不同团队技术栈/发布节奏严重割裂:强行统一会引发大量内耗;
  3. 没有工程化人力维护工具链:缓存、影响范围分析、依赖治理都做不好,Monorepo 反而拖慢所有人;
  4. 仓库文件巨大、二进制多:需要额外投入 Git LFS、浅克隆等基建。

一个务实的判断:从 pnpm workspace 起步,等痛点出现(构建太慢、需要缓存)再渐进引入 Turborepo/Nx,而不是一开始就上重型方案。


四、从 Multi-repo 迁移到 Monorepo

推荐渐进式迁移,避免"大爆炸式"合并:

Monorepo 博客介绍 (9).jpeg


五、总结

Monorepo 本质上是一种工程组织方式,它把"跨项目协作"的成本从"发布-安装-协调"转移到了"工具链建设"上:

  • 适合:多项目强关联、频繁共享代码、需要原子变更与统一治理的团队;
  • 不适合:项目彼此独立、追求极致自治、缺乏工程化投入的小团队;
  • 选型路径:小型起步 pnpm workspace → 需要任务编排加 Turborepo → 复杂企业级/需要精确影响范围分析选 Nx → 纯多包发布用 Changesets

工具会迭代,但底层理念稳定:共享、原子、统一、可缓存。理解这些机制,比追逐某个具体工具更重要。