01MVP 标识01MVP

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.js

4. 应用特定代码放在 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

核心原则

  1. 三层架构apps/ + packages/ + tooling/
  2. 统一管理:所有 packages 都有 package.json
  3. 依赖隔离:每个包声明自己的依赖
  4. 清晰分层
    • packages/ = 可复用业务代码
    • tooling/ = 开发工具
    • apps/*/lib/ = 应用特定代码
  5. 放弃 libs/:用 tooling/apps/*/lib/ 代替

快速决策树

代码需要放在哪里?

├─ 多个应用使用?
│  └─ 是 → packages/

├─ 开发时工具?
│  └─ 是 → tooling/

└─ 只有当前应用使用?
   └─ 是 → apps/*/src/lib/

记住:好的架构是清晰、一致、易维护的。遵循业界标准,避免过度设计。