随着项目规模的增长,你是否在多个仓库间疲于奔命?是否厌倦了重复的依赖安装和复杂的版本管理?是时候拥抱 Monorepo 了!
为什么我们需要 Monorepo?
在传统的 MultiRepo(多仓库)架构中,每个项目都有自己的独立仓库。这种模式在小型项目中工作良好,但随着业务复杂度增加,问题逐渐暴露:
-
代码共享困难:公共组件、工具函数需要在多个仓库间复制粘贴
-
依赖管理复杂:不同仓库的依赖版本不一致,导致诡异 bug
-
跨项目变更繁琐:一个需求需要修改多个仓库,提交、测试、发布流程复杂
-
开发环境配置重复:每个项目都要配置相似的构建工具、代码检查等
Monorepo(单一仓库) 将多个相关项目放在同一个代码仓库中,让它们可以共享代码、配置和工具链。
技术选型:为什么是 pnpm + Turborepo?
pnpm:下一代包管理器
与 npm/yarn 相比,pnpm 在 Monorepo 场景下具有显著优势:
-
磁盘空间高效:使用硬链接和符号链接,相同依赖只安装一次
-
安装速度快:依赖安装速度通常比 npm/yarn 快 2-3 倍
-
严格的依赖管理:避免幽灵依赖和非法依赖访问
-
内置 Workspace 支持:原生支持 Monorepo 工作空间
Turborepo:高性能的构建系统
Turborepo 专为 Monorepo 设计,解决了构建和任务执行的痛点:
-
增量构建:只构建发生变化的部分
-
并行执行:最大化利用多核 CPU
-
依赖图感知:智能识别任务依赖关系
-
远程缓存:团队共享构建缓存,避免重复工作
实战:从零搭建 Monorepo 项目
第一步:项目初始化
bash
mkdir my-monorepo && cd my-monorepo
pnpm init
创建 pnpm-workspace.yaml 文件,定义工作空间:
yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'
项目结构规划:
text
my-monorepo/
├── apps/
│ ├── web-app/ # 前端应用
│ └── admin-app/ # 后台应用
├── packages/
│ ├── ui/ # UI 组件库
│ ├── utils/ # 工具函数
│ └── types/ # 类型定义
├── tools/
│ └── eslint-config/ # ESLint 配置
└── package.json
第二步:配置 Turborepo
安装 Turborepo:
bash
pnpm add -Dw turbo
创建 turbo.json 配置文件:
json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"]
},
"lint": {
"dependsOn": ["^build"]
},
"type-check": {
"dependsOn": ["^build"]
}
}
}
第三步:创建共享包
1. 共享类型包 (packages/types)
bash
mkdir -p packages/types
cd packages/types
pnpm init
json
// packages/types/package.json
{
"name": "@my-project/types",
"version": "1.0.0",
"types": "index.ts",
"scripts": {
"build": "tsc --build",
"clean": "rm -rf dist"
}
}
typescript
// packages/types/index.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface ApiResponse<T> {
data: T;
success: boolean;
}
2. UI 组件库 (packages/ui)
bash
mkdir -p packages/ui
cd packages/ui
pnpm init
json
// packages/ui/package.json
{
"name": "@my-project/ui",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"dev": "tsup src/index.tsx --format esm,cjs --dts --external react --watch"
},
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"@my-project/types": "workspace:*",
"tsup": "^6.0.0"
}
}
tsx
// packages/ui/src/Button.tsx
import React from 'react';
import { ButtonHTMLAttributes } from '@my-project/types';
export const Button: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = ({
children,
...props
}) => {
return <button {...props}>{children}</button>;
};
第四步:创建应用
Web 应用 (apps/web-app)
bash
mkdir -p apps/web-app
cd apps/web-app
pnpm init
json
// apps/web-app/package.json
{
"name": "@my-project/web-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@my-project/ui": "workspace:*",
"@my-project/types": "workspace:*"
}
}
第五步:配置根目录脚本
json
// package.json (根目录)
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"test": "turbo run test",
"clean": "turbo run clean"
},
"devDependencies": {
"turbo": "^1.10.0"
}
}
高级特性与最佳实践
1. 依赖管理技巧
使用 pnpm 的过滤命令操作特定包:
bash
# 为所有包添加依赖
pnpm add -r lodash
# 为特定包添加依赖
pnpm add vite --filter @my-project/web-app
# 使用工作空间协议,确保总是使用本地版本
"dependencies": {
"@my-project/ui": "workspace:*"
}
2. 缓存配置优化
配置 Turborepo 远程缓存,实现团队构建共享:
bash
# 在 CI/CD 环境中
turbo run build --api="http://my-turborepo.com" --token="$TURBO_TOKEN"
3. 环境变量管理
创建共享环境配置:
typescript
// packages/config/src/env.ts
export const getEnvVars = () => ({
apiUrl: process.env.API_URL || 'http://localhost:3000',
environment: process.env.NODE_ENV || 'development',
});
4. CI/CD 流水线优化
yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install
- run: pnpm type-check
- run: pnpm lint
- run: pnpm test
- run: pnpm build
# 缓存构建输出
- uses: actions/cache@v3
with:
path: |
apps/*/dist
packages/*/dist
key: ${{ runner.os }}-build-${{ hashFiles('**/package.json', '**/tsconfig.json') }}
性能对比:传统方式 vs pnpm + Turborepo
| 场景 | 传统 MultiRepo | pnpm + Turborepo | 提升 |
|---|---|---|---|
| 初始安装 | 每个项目独立安装 | 依赖共享,硬链接 | 减少 60% 磁盘空间 |
| 构建时间 | 全量构建 | 增量构建 + 缓存 | 减少 70% 构建时间 |
| 开发启动 | 分别启动 | 并行启动 | 减少 50% 启动时间 |
| 跨项目变更 | 手动同步 | 自动依赖追踪 | 减少 80% 协调成本 |
常见问题与解决方案
Q: 如何处理不同项目的不同 Node.js 版本?
A: 使用 .nvmrc 文件 + volta 或 nvm 管理:
bash
# 在每个项目根目录创建 .nvmrc
echo "18" > apps/web-app/.nvmrc
echo "16" > apps/legacy-app/.nvmrc
Q: 如何管理私有包发布?
A: 使用 changesets 进行版本管理和发布:
bash
pnpm add -Dw @changesets/cli
pnpm changeset init
总结
通过 pnpm + Turborepo 的组合,我们成功构建了一个高效、可扩展的 Monorepo 架构:
-
🚀 极速体验:依赖安装和构建速度大幅提升
-
🔗 代码共享:轻松实现组件和工具函数的复用
-
🧩 模块化架构:清晰的包边界和依赖关系
-
⚡ 开发效率:热重载、并行执行提升开发体验
-
🛠 统一工具链:一致的代码质量和构建流程
Monorepo 不是银弹,但在中大型项目中,它能够显著提升团队的协作效率和开发体验。现在就开始重构你的项目架构,享受现代化开发工作流带来的便利吧!