Monorepo 架构指南
理解 Monorepo 的包管理策略和最佳实践
什么是 Monorepo
Monorepo(单一代码仓库)是将多个相关项目放在同一个 Git 仓库中管理的开发模式。本项目使用 Turborepo + pnpm workspace 实现 monorepo。
核心优势
- 代码共享:多个应用共享组件、工具和类型定义
- 统一依赖:所有包使用相同版本的依赖,避免版本冲突
- 原子提交:跨包的修改可以在一次 commit 中完成
- 统一工具链:共享 lint、format、test 配置
业界标准:三层架构
现代 monorepo 项目(如 next-forge、supastarter、turborepo 官方示例)普遍采用三层架构:
mono/
├── apps/ # 应用层
│ ├── web/
│ └── admin/
├── packages/ # 可复用业务包
│ ├── auth/
│ ├── database/
│ └── ui/
└── tooling/ # 开发工具
├── typescript/
├── scripts/
└── tailwind/1. packages/ - 可复用业务包
用途: 运行时业务逻辑,可被多个应用引用
packages/
├── auth/ # 认证逻辑
│ ├── package.json # 依赖:better-auth
│ └── src/
├── database/ # 数据库 schema
│ ├── package.json # 依赖:prisma
│ └── prisma/
├── ui/ # UI 组件库
│ ├── package.json # 依赖:react
│ └── src/
└── utils/ # 通用工具
├── package.json # 可能无外部依赖
└── src/特点:
- ✅ 每个包有独立的
package.json - ✅ 可以声明独立的依赖
- ✅ 使用
workspace:*引用其他包 - ✅ 可被多个 apps 引用
- ✅ 有明确的 API 边界
2. tooling/ - 开发工具配置
用途: 开发时工具和配置共享,不是运行时代码
tooling/
├── typescript/ # 共享 tsconfig
│ ├── package.json # @repo/tsconfig
│ ├── base.json
│ └── nextjs.json
├── scripts/ # 开发脚本
│ ├── package.json # @repo/scripts
│ └── src/
│ ├── create-user.ts
│ └── seed.ts
└── tailwind/ # 共享 tailwind 配置
├── package.json # @repo/tailwind
└── base.js特点:
- ✅ 每个工具有独立的
package.json - ✅ 可以引用
packages/(如脚本需要访问数据库) - ✅ 开发时使用,不打包到生产环境
- ✅ 配置共享和复用
3. apps/*/src/lib/ - 应用特定代码
用途: 只属于当前应用的业务逻辑
apps/mono-web/src/lib/
├── auth/ # 应用特定的认证配置
│ └── auth-config.ts
├── database/ # 应用特定的数据库操作
│ └── queries.ts
├── mail/ # 应用特定的邮件逻辑
│ └── templates/
└── payments/ # 应用特定的支付逻辑
└── stripe-config.ts特点:
- ✅ 无
package.json - ✅ 只属于当前应用
- ✅ 不跨应用复用
- ✅ 与应用强耦合
代码放置判断标准
放在 packages/ 的场景
判断标准:
- ✅ 多个 apps 需要使用
- ✅ 可以独立测试
- ✅ 有明确的 API 边界
- ✅ 有独立的外部依赖
- ✅ 可能未来发布到 npm
示例:
// packages/auth/package.json
{
"name": "@mono/auth",
"dependencies": {
"better-auth": "^1.4.0", // 独立依赖
"@mono/database": "workspace:*" // 引用其他包
}
}
// apps/mono-web 和 apps/admin 都可以使用
import { auth } from "@mono/auth";放在 tooling/ 的场景
判断标准:
- ✅ 开发时工具
- ✅ 配置文件
- ✅ 构建脚本
- ✅ 代码生成器
示例:
// tooling/scripts/package.json
{
"name": "@repo/scripts",
"scripts": {
"create:user": "tsx ./src/create-user.ts"
},
"dependencies": {
"@mono/database": "workspace:*" // 可以引用 packages
}
}
// 使用:pnpm --filter @repo/scripts create:user放在 apps/*/src/lib/ 的场景
判断标准:
- ✅ 只有当前应用使用
- ✅ 与应用强耦合
- ✅ 不需要跨应用复用
示例:
// apps/mono-web/src/lib/auth/auth-config.ts
import { betterAuth } from "@mono/auth"; // 引用 packages
export const auth = betterAuth({
// 应用特定配置
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});为什么需要独立的 package.json?
核心原因:依赖隔离
看一个实际例子:
// packages/auth/package.json
{
"name": "@mono/auth",
"dependencies": {
"better-auth": "^1.4.0", // auth 包特有
"@mono/database": "workspace:*"
}
}
// packages/tencent-cloud/package.json
{
"name": "@mono/tencent-cloud",
"dependencies": {
"tencentcloud-sdk-nodejs": "^4.1.0" // 腾讯云包特有
}
}如果没有独立 package.json:
- ❌ 所有依赖都在根
package.json - ❌ 即使不用腾讯云,也要安装 SDK
- ❌ 依赖污染,难以管理
使用独立 package.json:
- ✅ 每个包只安装需要的依赖
- ✅ 依赖关系清晰
- ✅ 更好的依赖树优化
本项目的推荐架构
基于当前项目的实际情况(有独立依赖需求),推荐采用三层架构:
mono/
├── apps/
│ └── mono-web/
│ └── src/
│ └── lib/ # 应用特定代码(无 package.json)
│ ├── auth/
│ ├── mail/
│ └── payments/
│
├── packages/ # 可复用业务包(有 package.json)
│ ├── auth/ # 依赖:better-auth
│ ├── tencent-cloud/ # 依赖:tencentcloud-sdk-nodejs
│ ├── storage/ # 依赖:AWS SDK
│ ├── ui/ # 依赖:react
│ └── utils/ # 无外部依赖,但保持一致性
│
└── tooling/ # 开发工具(有 package.json)
├── typescript/ # @repo/tsconfig
├── scripts/ # @repo/scripts
└── tailwind/ # @repo/tailwind最佳实践
1. 统一使用 packages/ + package.json
原则:保持一致性,所有可复用代码都放在 packages/,都有 package.json
✅ 推荐:统一管理
packages/
├── auth/ → 有 package.json(有外部依赖)
├── utils/ → 有 package.json(无外部依赖,但保持一致)
└── ui/ → 有 package.json(有外部依赖)
❌ 不推荐:混合方式
packages/
├── auth/ → 有 package.json
└── utils/ → 无 package.json(不一致)2. 完全放弃 libs/ 文件夹
原因:
- ❌ 语义不清晰(是库?是工具?)
- ❌ 引入第三种组织方式,增加复杂度
- ✅ 用
tooling/代替(明确是开发工具) - ✅ 用
apps/*/src/lib/代替(明确是应用代码)
3. 使用 tooling/ 管理开发工具
示例:
tooling/
├── typescript/
│ ├── package.json # @repo/tsconfig
│ ├── base.json
│ └── nextjs.json
├── scripts/
│ ├── package.json # @repo/scripts
│ └── src/
│ ├── create-user.ts
│ └── seed.ts
└── tailwind/
├── package.json # @repo/tailwind
└── base.js4. 应用特定代码放在 apps/*/src/lib/
示例:
// apps/mono-web/src/lib/auth/auth-config.ts
import { betterAuth } from "@mono/auth"; // 引用 packages
export const auth = betterAuth({
// 应用特定配置
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});5. 配置 pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"常见问题
Q: 为什么不用 libs/ 文件夹?
A: 语义不清晰,且与业界标准不符。
packages/- 明确是可复用的业务包tooling/- 明确是开发工具apps/*/lib/- 明确是应用特定代码libs/- 不清楚是什么,容易混淆
业界标准项目(next-forge、supastarter、turborepo)都采用 packages/ + tooling/ 的方式。
Q: 通用脚本应该放在哪里?
A: 放在 tooling/scripts/
// tooling/scripts/package.json
{
"name": "@repo/scripts",
"scripts": {
"create:user": "tsx ./src/create-user.ts",
"seed:db": "tsx ./src/seed.ts"
},
"dependencies": {
"@mono/database": "workspace:*" // 可以引用 packages
}
}使用:pnpm --filter @repo/scripts create:user
Q: 所有 packages 都需要 package.json 吗?
A: 是的,为了一致性。
即使某个包没有外部依赖,也建议添加 package.json:
// packages/utils/package.json
{
"name": "@mono/utils",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}好处:
- 统一管理方式
- 将来添加依赖时不需要重构
- Turborepo 更好地处理依赖图
Q: 如何在应用中引用包?
A: 在应用的 package.json 中声明依赖
// apps/mono-web/package.json
{
"dependencies": {
"@mono/auth": "workspace:*",
"@mono/ui": "workspace:*",
"@mono/utils": "workspace:*"
}
}代码中直接引用:
import { auth } from "@mono/auth";
import { Button } from "@mono/ui";
import { formatDate } from "@mono/utils";Q: 如何处理包之间的依赖?
A: 在 package.json 中使用 workspace:*
// packages/ui/package.json
{
"name": "@mono/ui",
"dependencies": {
"@mono/utils": "workspace:*", // 引用其他包
"react": "^19.0.0" // 外部依赖
}
}代码中直接引用:
// packages/ui/src/button.tsx
import { cn } from "@mono/utils"; // 自动解析
export function Button() { ... }Q: 什么时候应该拆分包?
A: 当代码有明确的边界和职责时。
应该拆分:
- ✅
@mono/auth- 认证相关(有独立依赖 better-auth) - ✅
@mono/database- 数据库 schema(有独立依赖 prisma) - ✅
@mono/ui- UI 组件库(有独立依赖 react)
不应该拆分:
- ❌ 只有 2-3 个函数的"微包"
- ❌ 只被一个应用使用的代码(应该放在
apps/*/lib/) - ❌ 没有明确边界的代码
Q: 项目特定的代码放在哪里?
A: 放在 apps/*/src/lib/
apps/mono-web/src/lib/
├── auth/
│ └── auth-config.ts # Web 应用特定的认证配置
├── mail/
│ └── templates/ # Web 应用特定的邮件模板
└── payments/
└── stripe-config.ts # Web 应用特定的支付配置判断标准:
- 只有当前应用使用 →
apps/*/lib/ - 多个应用共享 →
packages/
Q: 是否需要编译 packages?
A: 对于内部使用的包,通常不需要。
// packages/auth/package.json
{
"main": "./src/index.ts", // 直接引用源码
"types": "./src/index.ts" // 无需编译
}需要编译的场景:
- 发布到 npm
- 需要打包优化
- 需要兼容性转换
不需要编译的场景:
- 仅内部使用
- 应用会统一编译所有代码
- 开发体验更好(热更新更快)
总结
推荐架构
mono/
├── apps/ # 应用层
│ └── web/
│ └── src/lib/ # 应用特定代码(无 package.json)
│
├── packages/ # 可复用业务包(有 package.json)
│ ├── auth/ # 有外部依赖
│ ├── database/ # 有外部依赖
│ ├── ui/ # 有外部依赖
│ └── utils/ # 无外部依赖,但保持一致性
│
└── tooling/ # 开发工具(有 package.json)
├── typescript/ # @repo/tsconfig
├── scripts/ # @repo/scripts
└── tailwind/ # @repo/tailwind核心原则
- 三层架构:
apps/+packages/+tooling/ - 统一管理:所有 packages 都有 package.json
- 依赖隔离:每个包声明自己的依赖
- 清晰分层:
packages/= 可复用业务代码tooling/= 开发工具apps/*/lib/= 应用特定代码
- 放弃 libs/:用
tooling/和apps/*/lib/代替
快速决策树
代码需要放在哪里?
│
├─ 多个应用使用?
│ └─ 是 → packages/
│
├─ 开发时工具?
│ └─ 是 → tooling/
│
└─ 只有当前应用使用?
└─ 是 → apps/*/src/lib/记住:好的架构是清晰、一致、易维护的。遵循业界标准,避免过度设计。