Monorepo 架构以及工具选型、搭建
Monorepo(Monolithic Repository,单体仓库)是一种代码管理策略,核心是将一个项目的所有相关代码(包括多个应用、库、工具链等)集中存储在单个代码仓库中,而非按模块拆分到多个独立仓库(Multirepo)。
📑 目录
快速参考
工具选型速查表
| 工具类型 | 推荐工具 | 适用场景 | 备选方案 |
|---|---|---|---|
| 包管理器 | pnpm workspace | 磁盘效率高、安装速度快 | npm workspace、yarn workspace |
| 任务调度 | Turbo | 增量构建、并行任务、缓存 | Nx(企业级)、Rush(大型项目) |
| 版本管理 | Changeset | monorepo 友好的版本管理 | release-it(单包)、Lerna(传统) |
快速开始
# 1. 初始化项目
mkdir your-project && cd your-project
pnpm init -y
# 2. 安装 Turbo
pnpm add turbo -D -w
# 3. 配置工作区
# 创建 pnpm-workspace.yaml
# 4. 配置 Turbo
# 创建 turbo.json
# 5. 创建子包
mkdir -p packages/core docs examples/basic
什么是 Monorepo
简单类比
-
Multirepo:像多个独立的文件夹,每个项目 / 库单独存放(比如
react、react-dom、react-router各一个仓库) -
Monorepo:像一个大文件夹,里面按功能分类存放所有相关项目(比如 Facebook 的
facebook/react仓库,包含 React 核心、文档、示例、相关工具等所有代码)
常用结构
monorepo-root/
├── packages/ # 所有可复用包(库、工具)
│ ├── utils/ # 通用工具库
│ ├── components/ # UI 组件库
│ └── cli/ # 命令行工具
├── apps/ # 可部署应用
│ ├── web/ # 网页应用
│ └── admin/ # 管理后台
├── scripts/ # 全局构建/测试脚本
├── package.json # 根项目配置(依赖、脚本)
└── pnpm-workspace.yaml # 工作区配置(pnpm 为例)
Monorepo vs Multirepo
| 对比维度 | Multirepo(多仓库) | Monorepo(单仓库) |
|---|---|---|
| 依赖管理 | 重复安装,版本不一致,易冲突 | 共享依赖,版本统一,减少冗余 |
| 跨项目引用 | 需发布 npm / 用相对路径,同步修改繁琐 | 本地直接引用,修改实时生效,无需发布 |
| 工程化规范 | 各仓库独立配置,维护成本高 | 根目录统一配置,所有子项目继承 |
| 代码复用 | 复制粘贴或发布私有包,复用成本高 | 仓库内直接复用,抽离库更便捷 |
| 版本管理与发布 | 手动协调多包版本(如 A 依赖 B,B 升级后 A 需手动更新) | 工具自动管理版本依赖(如 Changeset),批量发布 |
| 协作效率 | 跨仓库 PR 联动复杂,代码审查分散 | 所有代码在一个仓库,PR 集中,协作更高效 |
Monorepo 的优缺点
优点:
- ✅ 高效协作:所有代码集中管理,跨项目修改无需切换仓库,PR 集中审查
- ✅ 规范统一:工程化配置(lint、测试、构建)全局统一,降低维护成本
- ✅ 依赖优化:共享依赖减少安装体积,版本统一避免冲突
- ✅ 代码复用:子包间直接引用,无需发布,迭代速度快
缺点:
- ❌ 仓库体积增大:随着项目增多,仓库体积会变大,但现代 Git(如 Git LFS)可缓解
- ❌ 构建速度:大型 Monorepo 全量构建较慢,需借助 Turborepo 等工具实现增量构建和缓存
- ❌ 权限控制:难以对单个子包进行精细化权限控制(如需控制,可结合 Git 子模块或企业级工具如 GitLab Enterprise)
经典案例
前端 Monorepo 经典案例:
-
React:
facebook/react仓库包含 React 核心、React DOM、React Server Components 等所有相关代码 -
Vue:
vuejs/core仓库包含 Vue 3 核心、编译器、运行时等 -
Vite:
vitejs/vite仓库包含 Vite 核心、官方插件(如@vitejs/plugin-react)等 -
Tailwind CSS:
tailwindlabs/tailwindcss仓库包含核心库、CLI、插件等
何时使用 Monorepo
当你的项目满足以下 2 个及以上条件时,优先选择 Monorepo:
- ✅ 需拆分独立模块(核心包 + 文档 + 示例是典型场景)
- ✅ 模块间有依赖关系(如示例依赖核心包、文档引用核心包 API)
- ✅ 需统一构建、测试、发布流程
- ✅ 追求高效开发(增量构建、并行任务)
何时不使用 Monorepo:
- ❌ 单一核心包 + 简单 README 文档(单包架构足够)
- ❌ 子包之间完全独立,无依赖关系
- ❌ 团队规模小,维护成本高
工具选型
工作区管理工具
负责管理多包的依赖安装、路径映射、脚本执行,主流选择:
| 工具 | 磁盘效率 | 安装速度 | monorepo 支持 | 适用场景 |
|---|---|---|---|---|
| pnpm workspace | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 推荐,生态最优 |
| npm workspace | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | npm 7+ 原生支持 |
| yarn workspace | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Yarn 1.x 传统方案 |
| Lerna | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 早期方案,已过时 |
选型建议:
- 推荐:pnpm workspace(磁盘效率高、安装速度快、生态完善)
- 备选:npm workspace(npm 7+ 原生支持,无需额外配置)
核心特性:
-
pnpm workspace:轻量、快速,原生支持 Monorepo,通过
pnpm-workspace.yaml配置子项目路径,自动处理包之间的软链接,安装依赖时复用缓存,效率极高 -
Yarn Workspaces:与 pnpm 功能类似,支持
workspace:*语法声明内部依赖 - Lerna:早期流行的 Monorepo 工具,可搭配 npm/yarn 使用,擅长版本管理和发布,但依赖安装效率不如 pnpm
任务调度工具
需支持「多包构建」「增量构建」「依赖顺序构建」,避免每次全量构建:
| 工具 | 增量构建 | 并行任务 | 缓存机制 | 配置复杂度 | 适用场景 |
|---|---|---|---|---|---|
| Turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 中小型项目(推荐) |
| Nx | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | 大型企业级项目 |
| Rush | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | 超大型项目 |
选型建议:
- 中小型项目:Turbo(推荐,配置简单,性能优秀)
- 大型企业级项目:Nx(功能强大,但配置复杂)
- 超大型项目:Rush(微软开源,适合超大型 monorepo)
核心特性:
- Turbo:高性能构建系统,可缓存构建结果,并行执行任务(构建、测试、lint),大幅提升大型 Monorepo 的构建速度
- tsup:支持多入口、增量构建,适配 TypeScript 项目,可快速构建多个子包
- Rollup/Vite:适合构建库或应用,支持 Tree-shaking,Vite 还能提供开发时热更新
版本管理工具
解决多包版本联动、CHANGELOG 自动生成、npm 发布等问题:
| 工具 | monorepo 支持 | 多包版本同步 | 自动化程度 | 配置复杂度 | 适用场景 |
|---|---|---|---|---|---|
| Changeset | ⭐⭐⭐⭐⭐ | ✅ 自动同步 | ⭐⭐⭐ | ⭐⭐⭐ | 多包项目(推荐) |
| release-it | ⭐⭐⭐ | ❌ 需手动 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 单包项目、简单场景 |
| Lerna | ⭐⭐⭐ | ✅ 支持 | ⭐⭐⭐ | ⭐⭐ | 传统方案 |
选型建议:
- 多包项目、需要版本同步:Changeset(推荐)
- 单包项目、追求自动化:release-it
- 简单场景、快速发布:release-it
核心特性:
- Changeset:轻量、易用,支持按子包提交变更记录,自动计算版本号(语义化版本),生成 CHANGELOG,批量发布子包
- release-it:可搭配 Changeset 使用,提供交互式发布流程,支持 GitHub 标签、发布说明等
代码质量工具
统一管理 lint、测试、格式化:
| 工具类型 | 推荐工具 | 核心功能 |
|---|---|---|
| 代码规范 | ESLint + Prettier | 根目录配置,所有子项目共享规则,可通过 eslint-config-xxx 抽离自定义规则 |
| 测试框架 | Vitest / Jest | 统一测试框架,支持跨包测试,可在根目录运行所有子项目的测试用例 |
| Git Hooks | Husky + lint-staged | 提交代码前自动执行 lint 和测试,保障代码质量 |
实战搭建
以下是最简搭建流程,基于 pnpm(生态最优)+ Turbo + tsup(核心包打包)+ VitePress(文档)+ Vitest(测试)。
1. 初始化基础环境
# 创建项目根目录
mkdir your-project && cd your-project
# 初始化根目录 package.json
pnpm init -y
# 安装 Turbo(任务调度)
pnpm add turbo -D -w # -w 表示安装到根目录(workspace-root)
2. 配置 pnpm 工作区
创建 pnpm-workspace.yaml:
# pnpm-workspace.yaml
packages:
- 'packages/*' # 核心包(可发布)
- 'docs' # 文档站点(不发布)
- 'examples/*' # 示例项目(不发布)
3. 配置 Turbo
创建 turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["package.json", "turbo.json"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "build/**", ".vitepress/dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"clean": {
"cache": false
}
}
}
配置说明:
-
dependsOn: ["^build"]:构建前先构建依赖的子包 -
outputs:指定构建输出目录,用于缓存判断 -
cache: false:开发模式不缓存,避免热更新问题 -
persistent: true:开发模式持续运行(如 watch 模式)
4. 根目录 package.json 配置
{
"name": "your-project-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md,json}\""
},
"devDependencies": {
"turbo": "^2.0.0",
"prettier": "^3.0.0"
},
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18.0.0",
"pnpm": ">=9.0.0"
}
}
5. 搭建核心包
# 创建核心包目录并初始化
mkdir -p packages/core && cd packages/core
pnpm init -y
# 安装核心依赖(共享依赖安装到根目录)
pnpm add -D tsup typescript vitest @types/node -w
核心包 package.json:
{
"name": "@your-org/core",
"version": "1.0.0",
"description": "核心功能包",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src/**/*.ts",
"clean": "rm -rf dist coverage"
},
"keywords": ["core", "utils"],
"license": "MIT"
}
核心包 tsup.config.ts:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
sourcemap: true,
minify: true,
splitting: false,
});
核心包 src/index.ts:
export const greet = (name: string) => {
return `Hello, ${name}!`;
};
export const add = (a: number, b: number) => {
return a + b;
};
6. 搭建文档站点
# 创建文档目录并初始化
mkdir docs && cd docs
pnpm init -y
# 安装文档依赖
pnpm add -D vitepress @vitepress/theme-default -w
# 初始化 VitePress 文档
npx vitepress init
文档 package.json:
{
"name": "@your-org/docs",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
"lint": "eslint . --ext .md,.ts",
"clean": "rm -rf .vitepress/dist"
},
"dependencies": {
"@your-org/core": "workspace:*"
}
}
在 Markdown 中可直接导入核心包,用于示例演示:
# 快速使用
```ts
import { greet } from '@your-org/core';
console.log(greet('World')); // Hello, World!
```
7. 搭建示例项目
# 创建示例目录并初始化
mkdir -p examples/basic && cd examples/basic
pnpm init -y
# 安装依赖(使用工作区协议引用核心包)
pnpm add @your-org/core@workspace:* -w
pnpm add -D vite @vitejs/plugin-react -w
示例 package.json:
{
"name": "@your-org/example-basic",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist"
},
"dependencies": {
"@your-org/core": "workspace:*"
}
}
8. 核心命令使用
| 命令 | 作用 |
|---|---|
pnpm run dev |
同时启动核心包监听、文档热更新、示例热更新 |
pnpm run build |
一键构建核心包(ESM/CJS + 类型)、文档静态资源、示例 |
pnpm run test |
运行所有子包的测试(核心包单元测试、示例冒烟测试) |
pnpm run lint |
统一校验所有子包的代码规范 |
pnpm run clean |
清理所有子包的构建产物和缓存 |
增量构建效果示例:
- 首次执行
pnpm run build:构建所有子包(core + docs + examples) - 修改核心包代码后再次执行
pnpm run build:仅重建 core 和依赖它的 examples,docs 未变更则直接复用缓存,构建速度提升 50%+
过滤特定子包执行任务:
# 只构建核心包
pnpm run build --filter=@your-org/core
# 只构建文档和示例
pnpm run build --filter=@your-org/docs --filter=@your-org/example-basic
# 构建核心包及其依赖者
pnpm run build --filter=@your-org/core...
9. 版本管理与发布
使用 Changeset(推荐,适合多包版本同步)
Changeset 完全支持 Turbo monorepo,可仅发布核心包(packages/core),文档和示例不发布,并支持多包版本同步:
优势:
- ✅ 专为 monorepo 设计,支持多包版本同步
- ✅ 自动更新依赖包的版本号
- ✅ 变更记录清晰,便于追溯
劣势:
- ❌ 需要手动记录变更(
npx changeset) - ❌ 流程相对复杂
安装与配置:
# 安装 Changeset 并初始化
pnpm add @changesets/cli -D -w
npx changeset init
修改 Changeset 配置(.changeset/config.json):
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@your-org/docs", "@your-org/example-basic"]
}
发布流程:
# 1. 记录核心包变更(仅选择 @your-org/core)
npx changeset
# 2. 升级版本 + 生成 CHANGELOG
npx changeset version
# 3. 构建核心包
pnpm run build --filter=@your-org/core
# 4. 发布核心包
pnpm publish --filter=@your-org/core --access public
使用 release-it(适合单包发布或简单场景)
release-it 也可以用于 monorepo,但主要用于单包发布场景。
优势:
- ✅ 自动化程度高,一条命令完成发布
- ✅ 配置灵活,支持自定义发布流程
- ✅ 支持 GitHub Release、npm 发布等
劣势:
- ❌ 不支持多包版本同步(需要手动处理)
- ❌ 需要为每个包单独配置或使用脚本
方式一:在子包中单独配置(推荐)
在需要发布的子包中配置 release-it:
# 在核心包目录下
cd packages/core
pnpm add release-it @release-it/conventional-changelog -D
核心包 release-it.config.js:
module.exports = {
git: {
commitMessage: 'chore: release @your-org/core@${version}',
tagName: '@your-org/core@${version}',
requireCleanWorkingDir: false,
requireBranch: 'main',
requireCommits: true,
},
github: {
release: true,
releaseName: '@your-org/core@${version}',
},
npm: {
publish: true,
publishPath: './',
},
hooks: {
'before:init': ['pnpm run test'],
'after:bump': ['pnpm run build'],
'after:release': 'echo "Release @your-org/core@${version} completed!"',
},
plugins: {
'@release-it/conventional-changelog': {
preset: 'angular',
infile: 'CHANGELOG.md',
},
},
};
核心包 package.json scripts:
{
"scripts": {
"release": "release-it",
"release:patch": "release-it patch",
"release:minor": "release-it minor",
"release:major": "release-it major"
}
}
发布流程:
# 在核心包目录下
cd packages/core
pnpm run release
方式二:在根目录统一配置(适合单包发布)
在根目录配置,配合 pnpm filter 使用:
# 在根目录安装
pnpm add release-it @release-it/conventional-changelog -D -w
根目录 release-it.config.js:
module.exports = {
git: {
commitMessage: 'chore: release v${version}',
tagName: 'v${version}',
requireCleanWorkingDir: false,
requireBranch: 'main',
},
hooks: {
'before:init': ['pnpm run test --filter=@your-org/core'],
'after:bump': ['pnpm run build --filter=@your-org/core'],
},
plugins: {
'@release-it/conventional-changelog': {
preset: 'angular',
infile: 'CHANGELOG.md',
},
},
};
根目录 package.json scripts:
{
"scripts": {
"release": "release-it",
"release:core": "cd packages/core && pnpm run release"
}
}
Changeset vs release-it 对比
| 特性 | Changeset | release-it |
|---|---|---|
| monorepo 支持 | ⭐⭐⭐⭐⭐(专为 monorepo 设计) | ⭐⭐⭐(需手动配置) |
| 多包版本同步 | ✅ 自动同步 | ❌ 需手动处理 |
| 自动化程度 | ⭐⭐⭐(需手动记录变更) | ⭐⭐⭐⭐⭐(一条命令) |
| 配置复杂度 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 适用场景 | 多包项目、版本同步需求 | 单包项目、简单场景 |
选型建议:
- 多包项目、需要版本同步:Changeset(推荐)
- 单包项目、追求自动化:release-it
- 简单场景、快速发布:release-it
高级配置
Turbo 缓存优化
1. 配置远程缓存(可选)
Turbo 支持远程缓存,团队共享构建缓存:
# 安装 Turbo 远程缓存客户端
pnpm add turbo -D -w
# 登录 Vercel(免费提供远程缓存)
npx turbo login
# 链接项目
npx turbo link
2. 优化缓存配置
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["NODE_ENV"], // 环境变量变化时重新构建
"inputs": ["src/**/*.ts", "tsup.config.ts"] // 指定输入文件
}
}
}
CI/CD 集成
GitHub Actions 示例
创建 .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Test
run: pnpm run test
- name: Lint
run: pnpm run lint
依赖管理策略
1. 公共依赖提升到根目录
# 安装公共依赖到根目录
pnpm add -D typescript eslint prettier -w
# 子包无需重复安装,直接使用
2. 子包间依赖使用工作区协议
{
"dependencies": {
"@your-org/core": "workspace:*" // ✅ 正确
// "@your-org/core": "1.0.0" // ❌ 错误,无法实时同步
}
}
3. 版本统一管理
使用 .npmrc 统一配置:
# .npmrc
shamefully-hoist=true
strict-peer-dependencies=false
最佳实践
-
子包命名规范:使用 scope(如
@your-org/core),避免命名冲突 -
依赖管理:公共依赖提升到根目录,子包间使用
workspace:*协议 -
任务配置:在
turbo.json中准确配置outputs,确保缓存生效 - 版本管理:使用 Changeset 管理版本,仅发布需要发布的包
- 目录结构:按功能拆分(packages、docs、examples),保持清晰
- 避免过度拆分:子包数量控制在 5 个以内,过多会增加配置复杂度
-
开发体验:使用
pnpm run dev同时启动所有子包的开发模式
常见问题
工作区相关问题
Q: 子包间依赖如何引用?
A: 使用工作区协议 workspace:*:
{
"dependencies": {
"@your-org/core": "workspace:*"
}
}
Q: 如何安装依赖到特定子包?
A:
# 安装到根目录
pnpm add -D typescript -w
# 安装到特定子包
pnpm add -D vite --filter @your-org/example-basic
# 安装到所有子包
pnpm add -D eslint --filter "./packages/*"
Q: 如何查看所有子包?
A:
pnpm list -r --depth=0
Turbo 相关问题
Q: Turbo 缓存不生效怎么办?
A:
- 检查
turbo.json中的outputs配置是否正确 - 检查构建输出目录是否在
outputs中声明 - 清理缓存:
pnpm run clean && rm -rf .turbo
Q: 如何只构建变更的子包?
A: Turbo 默认就是增量构建,只需执行 pnpm run build,Turbo 会自动判断哪些子包需要重新构建。
Q: 如何跳过缓存强制构建?
A:
pnpm run build --force
Q: 如何查看构建依赖关系?
A:
# 查看任务依赖图
npx turbo run build --graph
版本管理相关问题
Q: Changeset 如何只发布特定包?
A: 在 .changeset/config.json 中配置 ignore 字段,或在执行 npx changeset 时只选择需要发布的包。
Q: 如何自动化发布流程?
A: 使用 GitHub Actions + Changeset,参考 Changeset 文档。
Q: release-it 能否用于 monorepo?
A: 可以,但主要用于单包发布场景。如果项目只有一个包需要发布,release-it 更简单;如果需要多包版本同步,建议使用 Changeset。
Q: release-it 如何发布 monorepo 中的特定包?
A:
# 方式一:在子包目录下执行
cd packages/core
pnpm run release
# 方式二:使用 pnpm filter
pnpm --filter @your-org/core run release
性能优化相关问题
Q: 如何提升构建速度?
A:
- 配置 Turbo 远程缓存(团队共享)
- 优化
turbo.json的outputs配置 - 使用
dependsOn合理配置任务依赖 - 避免不必要的任务依赖
Q: 如何减少 node_modules 体积?
A:
- 使用 pnpm(默认使用符号链接,节省磁盘空间)
- 公共依赖提升到根目录
- 使用
.npmrc配置shamefully-hoist=false
参考资源
文档版本:v2.0
最后更新:2024 年