外观
Monorepo 系统学习文档
适合人群:已经做过单个前端或 Node.js 项目,准备管理多个应用、多个包、共享组件库、共享工具库或全栈工程的学习者。
学习目标:从 Monorepo 的基本概念开始,逐步掌握 workspace、依赖管理、包边界、任务编排、构建缓存、版本发布、CI/CD、工程规范和大型项目实践。
核对时间:2026-06-25。Monorepo 不是单一产品,本文综合 npm workspaces、pnpm workspaces、Turborepo、Nx 等官方资料整理。
官方资料:
- npm Workspaces:https://docs.npmjs.com/cli/v11/using-npm/workspaces/
- pnpm Workspaces:https://pnpm.io/workspaces
- Turborepo 文档:https://turborepo.com/docs
- Turborepo 仓库结构:https://turborepo.com/docs/crafting-your-repository/structuring-a-repository
- Nx 文档:https://nx.dev/docs
目录
第一篇:基础认知
- Monorepo 是什么
- Monorepo 和 Multirepo 的区别
- 为什么前端和全栈项目常用 Monorepo
- 适合与不适合的场景
- 常见工具生态
第二篇:Workspace 基础
- workspace 是什么
- npm workspaces
- pnpm workspaces
- 包之间如何互相依赖
- 根 package.json 的职责
第三篇:目录结构与包边界
- apps 和 packages
- 共享 UI 包
- 共享配置包
- 共享工具包
- TypeScript 项目引用
- package exports
第四篇:任务编排与构建
- scripts 管理
- Turborepo 基础
- Nx 基础
- 构建缓存
- 依赖图
- affected 构建思想
第五篇:发布、CI 与进阶实践
- 版本管理
- Changesets
- CI/CD 策略
- Docker 与部署
- 团队规范
- 常见误区
- 学习路线和实战项目
第一篇:基础认知
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.gitMonorepo:
text
company-platform.git
├── apps/web
├── apps/admin
├── apps/api
└── packages/ui对比:
| 维度 | Monorepo | Multirepo |
|---|---|---|
| 代码位置 | 一个仓库 | 多个仓库 |
| 跨项目修改 | 一个 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/uiworkspace 解决的是包依赖和安装问题。它不等于完整 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 buildpnpm 的 --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/utilspackages/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.jsonsrc/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.jsonbase.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 webTurborepo 适合希望快速获得任务缓存和流水线能力的 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 buildNx 适合:
- 项目规模较大。
- 团队需要强约束和生成器。
- 需要清晰项目图。
- 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/docsCI 中使用 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 -> buildMonorepo 优化后:
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,复杂后再引入更强工具。