Skip to content

Monorepo 系统学习文档 ​

适合人群:已经做过单个前端或 Node.js 项目,准备管理多个应用、多个包、共享组件库、共享工具库或全栈工程的学习者。

学习目标:从 Monorepo 的基本概念开始,逐步掌握 workspace、依赖管理、包边界、任务编排、构建缓存、版本发布、CI/CD、工程规范和大型项目实践。

核对时间:2026-06-25。Monorepo 不是单一产品,本文综合 npm workspaces、pnpm workspaces、Turborepo、Nx 等官方资料整理。

官方资料:


目录 ​

第一篇:基础认知 ​

  1. Monorepo 是什么
  2. Monorepo 和 Multirepo 的区别
  3. 为什么前端和全栈项目常用 Monorepo
  4. 适合与不适合的场景
  5. 常见工具生态

第二篇:Workspace 基础 ​

  1. workspace 是什么
  2. npm workspaces
  3. pnpm workspaces
  4. 包之间如何互相依赖
  5. 根 package.json 的职责

第三篇:目录结构与包边界 ​

  1. apps 和 packages
  2. 共享 UI 包
  3. 共享配置包
  4. 共享工具包
  5. TypeScript 项目引用
  6. package exports

第四篇:任务编排与构建 ​

  1. scripts 管理
  2. Turborepo 基础
  3. Nx 基础
  4. 构建缓存
  5. 依赖图
  6. affected 构建思想

第五篇:发布、CI 与进阶实践 ​

  1. 版本管理
  2. Changesets
  3. CI/CD 策略
  4. Docker 与部署
  5. 团队规范
  6. 常见误区
  7. 学习路线和实战项目

第一篇:基础认知 ​

1. Monorepo 是什么 ​

Monorepo 指把多个相关项目放在同一个 Git 仓库中管理。

例如:

text
my-company/
├── apps/
│   ├── web/
│   ├── admin/
│   └── api/
├── packages/
│   ├── ui/
│   ├── eslint-config/
│   ├── tsconfig/
│   └── utils/
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

这里 web、admin、api 是应用,ui、utils、tsconfig 是共享包。它们属于同一个仓库,可以一起开发、一起检查、一起构建,也可以分别发布和部署。

Monorepo 的本质不是“目录很多”,而是“多个项目在同一仓库内形成清晰依赖关系,并通过统一工具链管理”。

2. Monorepo 和 Multirepo 的区别 ​

Multirepo:

text
web-app.git
admin-app.git
api-server.git
ui-library.git
utils.git

Monorepo:

text
company-platform.git
├── apps/web
├── apps/admin
├── apps/api
└── packages/ui

对比:

维度MonorepoMultirepo
代码位置一个仓库多个仓库
跨项目修改一个 PR 可完成需要多个 PR
依赖联调本地直接引用需要发布或 link
工具统一更容易统一容易分散
仓库体积可能较大单仓较小
权限隔离较弱更容易隔离

如果多个项目之间强相关,经常需要同步修改,Monorepo 更适合。如果项目完全独立、权限边界严格、团队彼此隔离,Multirepo 可能更简单。

3. 为什么前端和全栈项目常用 Monorepo ​

现代前端项目经常包含:

  • 用户端 Web
  • 管理后台
  • 移动端 H5
  • Node.js API
  • 组件库
  • 工具函数库
  • ESLint 配置
  • TypeScript 配置
  • 数据库 schema 或 SDK

如果这些项目分散在多个仓库中,共享代码会很麻烦。例如 UI 组件库改一个 Button,需要发布版本,业务项目再升级依赖。Monorepo 中可以直接在同一个分支里修改组件库和使用方。

优势:

  • 原子化修改:共享包和应用可以在一个提交中同时改。
  • 统一规范:lint、format、tsconfig、测试规则更容易保持一致。
  • 本地联调:不需要频繁 npm publish。
  • 依赖关系可视化:工具可以分析哪些包受影响。
  • CI 优化:只构建受影响的项目。

4. 适合与不适合的场景 ​

适合:

  • 多个前端应用共享 UI 和工具代码。
  • 前后端在同一团队内紧密协作。
  • 组件库和业务项目需要频繁联调。
  • 团队想统一 TypeScript、ESLint、Prettier、测试和构建规范。
  • 平台型项目,例如 SaaS 的 web、admin、docs、api、sdk。

不适合:

  • 只有一个小项目,没有共享需求。
  • 各项目技术栈完全不同且很少协作。
  • 权限隔离是第一优先级。
  • 团队没有能力维护统一工程规范。
  • CI/CD 基础薄弱,仓库变大后没人优化。

5. 常见工具生态 ​

包管理器 workspace:

  • npm workspaces
  • pnpm workspaces
  • Yarn workspaces

任务编排和缓存:

  • Turborepo
  • Nx
  • Lage

版本发布:

  • Changesets
  • semantic-release
  • pnpm publish
  • npm publish

常见组合:

text
pnpm workspace + Turborepo + Changesets

或者:

text
pnpm workspace + Nx

简单项目先掌握 workspace;项目变复杂后再引入 Turborepo 或 Nx。

5.1 包管理器安装模型差异 ​

同样是 workspace,npm、pnpm、Yarn 安装出来的 node_modules 结构差别很大,直接影响依赖能不能被“意外引用”:

包管理器node_modules 结构特点
npm扁平化(flat)所有依赖都提升到根 node_modules,能访问到未声明的依赖
pnpm内容寻址 + 符号链接(symlink)只暴露 package.json 里显式声明的依赖,其余放在 .pnpm 内部
Yarn (node-modules linker)扁平化与 npm 类似
Yarn (PnP)无 node_modules通过 .pnp.cjs 解析,部分工具链需要额外适配

pnpm 的严格模型对 Monorepo 特别重要:

text
问题:apps/web 的代码里写了 import _ from "lodash",
但 apps/web/package.json 没有声明 lodash,
只是 packages/utils 声明了。

扁平化安装(npm):能跑,因为 lodash 被提升到了根目录 -> 幽灵依赖(phantom dependency)
pnpm 严格安装:直接报找不到模块 -> 问题在安装阶段就暴露

⚠️ 幽灵依赖是 Monorepo 的常见隐患:本地能跑,但包单独发布或换机器安装就坏。pnpm 从结构上避免了大多数幽灵依赖;用 npm 时则需要靠 lint(如 import/no-extraneous-dependencies)和代码评审兜底。

另外,pnpm 的全局内容寻址存储(默认在 ~/.pnpm-store 或平台等价位置)让同一个包在磁盘上只存一份,多个项目共享,安装速度和磁盘占用都更优。


第二篇:Workspace 基础 ​

6. workspace 是什么 ​

workspace 是包管理器提供的多包管理能力。它让根目录可以统一安装依赖,并让子包之间通过本地链接互相引用。

没有 workspace 时:

text
web 依赖 ui,需要先发布 ui 到 npm,再安装

有 workspace 时:

text
web 依赖 @repo/ui,本地开发时直接链接 packages/ui

workspace 解决的是包依赖和安装问题。它不等于完整 Monorepo 工程体系。完整体系还包括任务编排、缓存、测试、发布、权限、CI/CD 等。

7. npm workspaces ​

根 package.json:

json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "dev": "npm run dev --workspaces",
    "build": "npm run build --workspaces"
  }
}

子包:

json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "main": "./dist/index.js"
}

应用依赖共享包:

json
{
  "dependencies": {
    "@repo/ui": "*"
  }
}

npm workspaces 上手简单,适合基础 Monorepo。大型项目里,pnpm 因依赖安装模型和 workspace 能力更强,也很常见。

8. pnpm workspaces ​

pnpm workspace 需要 pnpm-workspace.yaml。

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

根 package.json:

json
{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@10.0.0",
  "scripts": {
    "dev": "pnpm -r dev",
    "build": "pnpm -r build"
  }
}

添加依赖到某个应用:

bash
pnpm --filter web add react

添加 workspace 内部包:

bash
pnpm --filter web add @repo/ui --workspace

运行某个包脚本:

bash
pnpm --filter web dev

递归运行:

bash
pnpm -r build

pnpm 的 --filter 是 Monorepo 中非常重要的能力,可以精确选择某个包、某类包或受影响包。

8.1 --filter 选择器语法详解 ​

--filter(或 -F)是 pnpm workspace 的核心,理解它的选择器语法,CI 和日常命令都会高效很多:

bash
# 按包名精确选择(scope 可省略,但同名包多时会匹配失败)
pnpm --filter @repo/web dev
pnpm --filter web dev

# 按通配符选择一组包
pnpm --filter "@repo/*" lint

# 包 + 它的所有依赖(后缀 ...)
pnpm --filter @repo/web... build

# 只选它的依赖(^... 表示不含自己)
pnpm --filter "@repo/web^..." build

# 包 + 所有依赖它的包(前缀 ...)——改公共包时最常用
pnpm --filter ...@repo/ui test

# 只选依赖它的包
pnpm --filter "...^@repo/ui" test

# 按目录选择
pnpm --filter "./apps/**" build

# 选择相对某个分支有变更的包(CI 最常用)
pnpm --filter "...[origin/main]" test

组合用法:

bash
# 变更包及其所有下游,并跑测试
pnpm --filter "...[origin/main]" test

# 排除某个包(前缀 !,zsh 里可能需要转义 \!)
pnpm --filter "!@repo/docs" build

# 配合 --test-pattern:变更只在测试文件时,不再把下游包拉进来
pnpm --filter "...[origin/main]" --test-pattern "test/**" test

# 配合 --changed-files-ignore-pattern:忽略文档类变更
pnpm --filter "...[origin/main]" --changed-files-ignore-pattern "**/*.md" run build

建议:

  • 日常开发用包名过滤;CI 用 [origin/main] 变更过滤 + 下游扩展。
  • 给根脚本封装常用过滤命令,避免每个人写一长串参数。

9. 包之间如何互相依赖 ​

目录:

text
apps/web
packages/ui
packages/utils

packages/ui/package.json:

json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  }
}

apps/web/package.json:

json
{
  "dependencies": {
    "@repo/ui": "workspace:*"
  }
}

使用:

tsx
import { Button } from "@repo/ui";

workspace:* 表示依赖 workspace 内部包,而不是去 npm registry 下载远程包。

10. 根 package.json 的职责 ​

根 package.json 通常负责:

  • 声明 workspace 范围。
  • 统一 package manager 版本。
  • 定义全局脚本。
  • 放置开发依赖,如 TypeScript、ESLint、Prettier、Turbo。
  • 配置 lint-staged、husky 等工程工具。

根目录不应该承载具体业务代码。具体应用放在 apps,共享能力放在 packages。

根目录示例:

json
{
  "name": "company-platform",
  "private": true,
  "packageManager": "pnpm@10.0.0",
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "typecheck": "turbo typecheck"
  },
  "devDependencies": {
    "turbo": "latest",
    "typescript": "latest",
    "prettier": "latest"
  }
}

第三篇:目录结构与包边界 ​

11. apps 和 packages ​

常见约定:

text
apps/
  web/       用户端应用
  admin/     管理后台
  api/       后端服务
  docs/      文档站

packages/
  ui/              共享组件
  utils/           工具函数
  config-eslint/   ESLint 配置
  config-ts/       TS 配置
  database/        Prisma schema 或数据库访问层

apps 放可独立运行或部署的应用。packages 放被应用依赖的共享包。

判断一个目录应该放哪里:

  • 能不能独立部署?能,放 apps。
  • 是不是被多个应用复用?是,放 packages。
  • 只是某个应用内部模块?留在该应用内部,不要过早抽到 packages。

12. 共享 UI 包 ​

packages/ui 示例:

text
packages/ui/
├── src/
│   ├── button.tsx
│   ├── input.tsx
│   └── index.ts
├── package.json
└── tsconfig.json

src/index.ts:

ts
export * from "./button";
export * from "./input";

package.json:

json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

为什么 React 放 peerDependencies:

  • UI 包不应该自己捆绑一份 React。
  • 应用负责提供 React 版本。
  • 避免多个 React 实例导致 hooks 错误。

13. 共享配置包 ​

共享 TypeScript 配置:

text
packages/tsconfig/
├── base.json
├── react.json
└── package.json

base.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true
  }
}

应用中继承:

json
{
  "extends": "@repo/tsconfig/react.json",
  "include": ["src"]
}

共享 ESLint 配置同理。这样做的价值是统一规则,避免每个项目复制一份配置后逐渐漂移。

14. 共享工具包 ​

packages/utils 放纯函数、类型、通用常量。

示例:

ts
export function formatCurrency(value: number, currency = "CNY") {
  return new Intl.NumberFormat("zh-CN", {
    style: "currency",
    currency,
  }).format(value);
}

注意:

  • 工具包不要依赖具体应用。
  • 不要把业务逻辑随意塞进 utils。
  • 如果函数只被一个应用使用,先留在应用内部。
  • 公共包 API 要保持稳定,因为修改会影响多个消费者。

15. TypeScript 项目引用 ​

大型 Monorepo 中,TypeScript 编译可能变慢。Project References 可以让 TS 按项目拆分构建。

根 tsconfig.json:

json
{
  "files": [],
  "references": [
    { "path": "./packages/utils" },
    { "path": "./packages/ui" },
    { "path": "./apps/web" }
  ]
}

子包:

json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

优点:

  • 类型检查可增量构建。
  • 包依赖关系更清晰。
  • 对大型仓库更友好。

但小项目不必一开始就引入,避免复杂度过早上升。

16. package exports ​

exports 控制包对外暴露什么。

json
{
  "name": "@repo/utils",
  "type": "module",
  "exports": {
    ".": "./src/index.ts",
    "./date": "./src/date.ts",
    "./currency": "./src/currency.ts"
  }
}

使用:

ts
import { formatCurrency } from "@repo/utils/currency";

好处:

  • 明确公共 API。
  • 避免消费者随意引用内部文件。
  • 方便未来重构内部目录。

不要让应用这样引用:

ts
import { x } from "@repo/utils/src/internal/deep/file";

这会破坏包边界。

16.1 条件导出与发布时的产物路径 ​

真实发布的包,exports 通常还要区分开发/生产、不同模块系统:

json
{
  "name": "@repo/ui",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    },
    "./button": {
      "types": "./dist/button.d.ts",
      "import": "./dist/button.js"
    }
  }
}

要点:

  • types 条件要放在最前面,TypeScript 才能正确解析类型。
  • 条件按顺序匹配,import / require / default 是常见组合。
  • exports 一经发布就是公共 API,删掉或改路径属于破坏性变更,要升 major。

开发期与发布期的区别:

text
内部使用(不发布):
  exports 可以直接指向源码,例如 ".": "./src/index.ts"
  由 Vite / Next / Vitest 等构建工具即时编译,改动立即生效

发布到 npm:
  exports 必须指向编译产物(dist)+ 类型声明(.d.ts)
  消费者不需要也不应该编译你的源码

⚠️ 常见错误:内部包配置了 "exports": { ".": "./src/index.ts" },直接 npm publish 后,外部消费者拿到的是没有编译的 TS 源码,构建直接失败。发布前要么改成产物路径,要么用发布脚本在 prepublishOnly 阶段构建。


第四篇:任务编排与构建 ​

17. scripts 管理 ​

每个包都可以有自己的脚本:

json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  }
}

根目录统一调用:

json
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "typecheck": "turbo typecheck"
  }
}

关键原则:

  • 子包定义自己能做什么。
  • 根目录负责任务编排。
  • 不要在根脚本里写死每个包的内部命令。

18. Turborepo 基础 ​

Turborepo 是常见的 JavaScript/TypeScript Monorepo 任务编排工具。它建立在 package manager workspaces 之上。

turbo.json 示例:

json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

解释:

  • dependsOn: ["^build"] 表示先构建依赖包。
  • outputs 告诉 Turbo 哪些构建产物可以缓存。
  • dev 通常是长期运行任务,不缓存。

运行:

bash
pnpm turbo build
pnpm turbo lint
pnpm turbo dev --filter web

Turborepo 适合希望快速获得任务缓存和流水线能力的 JS/TS Monorepo。

18.1 任务配置的关键字段与过滤运行 ​

turbo.json 里每个任务都可以声明依赖、输入、输出和运行模式:

json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"],
      "outputs": ["dist/**"],
      "env": ["VITE_CDN_BASE"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

字段含义:

字段作用
dependsOn: ["^build"]^ 表示“先运行依赖包的该任务”;不带 ^ 表示自己包内的其他任务
inputs参与缓存哈希的文件范围,缩小它可提高缓存命中率
outputs缓存命中时直接恢复的产物
env影响任务结果的环境变量,必须声明,否则缓存可能错误复用
cache: false关闭缓存(dev、watch 类任务必须关)
persistent: true声明为长期运行任务,不会被当作可结束的任务等待

运行时可以精确过滤:

bash
# 只跑某个包的任务
pnpm turbo build --filter=@repo/web

# 某个包 + 依赖它的包(改公共包时验证下游)
pnpm turbo build --filter=...@repo/ui

# 只跑相对主分支有变更的包(CI 最常用)
pnpm turbo build --filter=[origin/main]

# 按目录过滤
pnpm turbo lint --filter="./apps/*"

💡 Turborepo 会自动并行没有依赖关系的任务,并按任务图顺序执行有依赖的任务;你不需要手写并行脚本。

19. Nx 基础 ​

Nx 是功能更完整的 Monorepo 工具,支持项目图、代码生成、缓存、affected 命令、插件体系和多技术栈。

常见命令:

bash
nx graph
nx run web:build
nx affected -t build

Nx 适合:

  • 项目规模较大。
  • 团队需要强约束和生成器。
  • 需要清晰项目图。
  • CI 中需要精准 affected 构建。

Turborepo 和 Nx 如何选择:

  • 想轻量、贴近 package scripts:优先 Turborepo。
  • 想要更完整的项目治理、生成器和图分析:考虑 Nx。

20. 构建缓存 ​

构建缓存的思想:同样输入产生同样输出时,不必重复构建。

缓存输入通常包括:

  • 源代码
  • package.json
  • lockfile
  • 环境变量
  • 配置文件

缓存输出通常包括:

  • dist/**
  • .next/**
  • coverage/**
  • 构建生成文件

缓存价值:

  • 本地二次构建更快。
  • CI 可以复用历史结果。
  • 大型 Monorepo 不必每次从零构建所有项目。

注意:如果任务依赖环境变量,必须让工具知道这些环境变量,否则可能出现错误复用缓存。

20.1 缓存的工作原理与失效排查 ​

以 Turborepo 为例,缓存命中判断大致是:

text
缓存 key = hash(任务 id + inputs 文件内容 + 依赖任务的输出 + 声明的 env + 全局配置)

命中:直接把 outputs 恢复到对应包目录,跳过执行
未命中:真正执行任务,执行完成后写入缓存

本地缓存默认在仓库内的 .turbo 目录(以及全局缓存目录);配置 Remote Cache 后,CI 跑过的结果本地也能复用:

bash
# 登录并启用远程缓存(Turborepo 官方托管或自托管)
npx turbo login
npx turbo link

缓存错误的典型症状与原因:

症状常见原因修复
改了环境变量,结果没变该变量没有写进任务的 env补充 "env": ["MY_VAR"]
改了某个配置文件,缓存仍命中文件不在 inputs 范围内把配置文件加入 inputs
产物少文件outputs 声明不完整补全 outputs glob
dev/watch 任务行为怪异错误地开启了缓存"cache": false, "persistent": true

排查手段:

bash
# 查看任务为什么命中/未命中缓存(显示 hash 和依赖)
pnpm turbo build --filter=@repo/web --summarize
pnpm turbo build --dry=json   # 只打印将要执行的任务,不执行

⚠️ 缓存是“输入相同则输出相同”的承诺。任何影响结果却没有声明的输入(环境变量、隐藏文件、网络请求)都会破坏这个承诺。宁可 inputs 多写,不要漏写。

21. 依赖图 ​

依赖图描述项目之间的依赖关系。

示例:

text
apps/web
├── packages/ui
└── packages/utils

packages/ui
└── packages/utils

如果修改 packages/utils,那么 packages/ui 和 apps/web 都可能受影响。

依赖图能帮助:

  • 决定构建顺序。
  • 判断哪些测试需要运行。
  • 找出循环依赖。
  • 评估某个包的影响范围。

循环依赖是 Monorepo 中需要警惕的问题:

text
packages/ui -> packages/utils -> packages/ui

这通常说明包边界设计不清晰。

22. affected 构建思想 ​

affected 的核心问题是:这次改动影响了哪些项目?

例如只改了 apps/admin 页面:

text
只需要 lint/test/build admin

如果改了 packages/ui/Button:

text
需要检查 ui,以及依赖 ui 的 web/admin/docs

CI 中使用 affected 可以显著减少耗时。Nx 对 affected 支持很强,Turborepo 也可以通过 filter、Git 范围和缓存实现类似效果。


第五篇:发布、CI 与进阶实践 ​

23. 版本管理 ​

Monorepo 有两种常见版本策略。

固定版本:

text
@repo/ui@1.2.0
@repo/utils@1.2.0
@repo/config@1.2.0

独立版本:

text
@repo/ui@1.4.0
@repo/utils@0.8.1
@repo/config@0.2.0

固定版本适合包之间强绑定的项目;独立版本适合包可以单独演进和发布的项目。

如果包只在仓库内部使用,不发布到 npm,可以长期使用 0.0.0 加 workspace:*。

24. Changesets ​

Changesets 常用于管理多包版本和 changelog。

基本流程:

bash
pnpm changeset
pnpm changeset version
pnpm changeset publish

它解决:

  • 哪些包需要发版。
  • 是 patch、minor 还是 major。
  • changelog 如何生成。
  • 内部依赖版本如何同步更新。

适合组件库、SDK、工具库等需要发布到 npm 的 Monorepo。

业务应用如果只部署不发布 npm 包,不一定需要 Changesets。

25. CI/CD 策略 ​

基础 CI:

text
install -> lint -> typecheck -> test -> build

Monorepo 优化后:

text
install -> detect affected -> lint affected -> test affected -> build affected

建议:

  • 使用 lockfile 保证依赖一致。
  • 缓存 pnpm store 或 npm cache。
  • 缓存 Turbo/Nx 构建结果。
  • PR 阶段跑检查,main 分支负责部署。
  • 每个应用独立部署,不要因为 admin 改动重新部署 web。

CI 常见问题:

  • 本地能跑,CI 不能跑:通常是环境变量、大小写路径、Node 版本、lockfile 不一致。
  • 缓存命中但结果错误:通常是 outputs 或 env 配置不完整。
  • 所有项目每次都构建:没有使用 affected 或 filter。

26. Docker 与部署 ​

Monorepo 部署时要避免把整个仓库无脑复制进镜像。

目标:

  • 只复制当前应用需要的代码和依赖。
  • 使用构建缓存。
  • 减少镜像体积。

Turborepo 有 prune 思想,可以裁剪某个应用需要的依赖子集。即使不用 Turbo,也应理解部署边界:

text
部署 apps/web 时,需要:
- apps/web
- web 依赖的 packages
- 根 lockfile
- 必要配置文件

不要把测试文件、文档、大量无关应用都打进生产镜像。

27. 团队规范 ​

建议制定:

  • 目录规范:apps/*, packages/*。
  • 命名规范:内部包统一 @repo/* 或公司 scope。
  • 依赖规范:应用可依赖 packages,packages 不反向依赖 apps。
  • 脚本规范:所有包统一 build, lint, test, typecheck。
  • 版本规范:哪些包发布,哪些包只内部使用。
  • API 规范:只允许通过 exports 暴露公共 API。
  • 评审规范:修改共享包必须检查影响范围。

推荐边界:

text
apps/* 可以依赖 packages/*
packages/ui 可以依赖 packages/utils
packages/utils 不应该依赖 packages/ui
packages/* 不应该依赖 apps/*

28. 常见误区 ​

误区一:Monorepo 就是把所有项目放到一个仓库。

纠正:真正的 Monorepo 需要 workspace、包边界、任务编排、缓存、CI 和规范。

误区二:一开始就上最复杂工具。

纠正:小项目可以从 pnpm workspace 开始,复杂后再加 Turbo 或 Nx。

误区三:所有公共代码都应该抽包。

纠正:只有稳定、复用明确、边界清晰的代码才适合抽包。

误区四:共享包可以随便改,因为都在一个仓库。

纠正:共享包影响范围更大,修改更需要测试和评审。

误区五:Monorepo 一定比 Multirepo 好。

纠正:它只是组织方式。项目关系、团队结构、权限和 CI 能力决定是否适合。

29. 学习路线和实战项目 ​

第一阶段:理解 workspace

  • 创建 apps/web 和 packages/utils。
  • 用 pnpm workspace 让 web 引用 utils。
  • 学会 pnpm --filter。

第二阶段:共享配置

  • 抽离 @repo/tsconfig。
  • 抽离 @repo/eslint-config。
  • 统一 lint 和 typecheck。

第三阶段:共享 UI

  • 创建 @repo/ui。
  • 封装 Button、Input、Modal。
  • 在 web 和 admin 中复用。

第四阶段:任务编排

  • 引入 Turborepo。
  • 配置 build, lint, typecheck。
  • 理解 dependsOn 和 outputs。

第五阶段:CI 和发布

  • 配置 PR 检查。
  • 只构建受影响项目。
  • 对可发布包引入 Changesets。

实战项目:

text
fullstack-platform/
├── apps/
│   ├── web/        用户端
│   ├── admin/      后台管理
│   └── api/        Node.js API
├── packages/
│   ├── ui/         Tailwind + React 组件
│   ├── utils/      工具函数
│   ├── database/   Prisma schema 和 Client
│   ├── tsconfig/   TS 配置
│   └── eslint-config/
├── pnpm-workspace.yaml
├── turbo.json
└── package.json

练习目标:

  • web 和 admin 共享 UI。
  • api 和 web 共享类型。
  • database 包统一 Prisma schema。
  • Turbo 编排 build/lint/typecheck。
  • CI 只检查受影响项目。

快速记忆清单 ​

  • Monorepo 是多项目单仓库,不只是目录合并。
  • workspace 解决本地多包依赖和安装问题。
  • apps 放应用,packages 放共享包。
  • 共享包要有清晰 API,不允许随意深层引用。
  • Turborepo/Nx 解决任务编排、缓存和依赖图问题。
  • 构建缓存必须正确声明输入、输出和环境变量。
  • 修改共享包要检查所有受影响应用。
  • 小项目先用 workspace,复杂后再引入更强工具。

基于 VitePress 构建的个人学习文档知识库