一、为什么需要 Monorepo?
当团队项目发展到一定阶段,你会遇到这些问题:
- 公共逻辑复制粘贴:工具函数在 5 个项目里各有一份,改 bug 要改 5 次
- 版本管理混乱:A 项目依赖 UI 库 1.2.0,B 项目依赖 1.3.0,C 项目直接本地 copy 了一份
- 联调成本高昂:改一个公共组件,要 npm link 到各个项目测试
- CI/CD 重复配置:每个仓库配一套流水线,维护 nightmare
Monorepo 的核心价值:在一个仓库中管理多个相关项目,代码共享、依赖统一、原子提交。
二、Monorepo 方案对比
| 方案 | 工具链 | 优点 | 缺点 | 适用规模 |
|---|---|---|---|---|
| Lerna | yarn/npm | 生态成熟 | 性能差、维护慢 | 中小型 |
| Nx | 专用 CLI | 功能强大 | 学习曲线陡 | 大型 |
| Turborepo | pnpm/npm | 极速、云缓存 | 相对较新 | 中大型 |
| Rush | 微软出品 | 企业级 | 配置复杂 | 超大型 |
本文选择 pnpm + Turborepo,理由:速度快、配置简单、社区活跃。
三、项目初始化与架构设计
3.1 目录结构设计
my-monorepo/
├── apps/ # 应用层
│ ├── web-admin/ # 管理后台
│ ├── web-portal/ # 用户端
│ └── mobile-h5/ # H5 页面
├── packages/ # 共享包
│ ├── ui/ # 组件库
│ ├── utils/ # 工具函数
│ ├── hooks/ # 共享 hooks
│ ├── eslint-config/ # 共享 ESLint 配置
│ └── ts-config/ # 共享 TS 配置
├── turbo.json # Turborepo 配置
├── pnpm-workspace.yaml # pnpm 工作区
└── package.json
3.2 pnpm 工作区配置
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# 依赖提升规则(避免重复安装)
shared-workspace-lockfile: true
link-workspace-packages: true
// package.json (根目录)
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@8.15.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter=docs^... && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"turbo": "^1.12.0"
}
}
3.3 Turborepo 管道配置
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
}
}
}
关键概念解释:
^build:依赖项需要先构建(拓扑排序执行)outputs:缓存这些目录,下次直接复用cache: false:不缓存,实时执行
四、共享包开发实战
4.1 创建 UI 组件库
// packages/ui/src/components/Button.tsx
import React from 'react';
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
onClick
}) => {
const baseStyles = 'rounded font-medium transition-colors';
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizes = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
onClick={onClick}
>
{children}
</button>
);
};
// packages/ui/src/components/Button.tsx
import React from 'react';
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
onClick
}) => {
const baseStyles = 'rounded font-medium transition-colors';
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizes = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
onClick={onClick}
>
{children}
</button>
);
};
// packages/ui/src/index.ts
export { Button } from './components/Button';
export { Input } from './components/Input';
export { Modal } from './components/Modal';
// ...
4.2 共享配置包
// packages/eslint-config/index.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
settings: {
react: {
version: 'detect'
}
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off'
}
};
// packages/eslint-config/package.json
{
"name": "@myrepo/eslint-config",
"version": "0.0.1",
"main": "index.js",
"files": ["*.js"],
"dependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0"
}
}
// apps/web-admin/.eslintrc.js
{
"extends": ["@myrepo/eslint-config"]
}
五、应用层开发
5.1 管理后台项目
// apps/web-admin/package.json
{
"name": "@myrepo/web-admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@myrepo/ui": "workspace:*",
"@myrepo/utils": "workspace:*",
"@myrepo/hooks": "workspace:*",
"next": "14.1.0",
"react": "^18.2.0"
},
"devDependencies": {
"@myrepo/tsconfig": "workspace:*",
"@myrepo/eslint-config": "workspace:*"
}
}
// apps/web-admin/src/pages/index.tsx
import { Button } from '@myrepo/ui';
import { formatDate } from '@myrepo/utils';
import { useAuth } from '@myrepo/hooks';
export default function Dashboard() {
const { user } = useAuth();
return (
<div>
<h1>管理后台</h1>
<p>当前时间:{formatDate(new Date())}</p>
<p>欢迎,{user?.name}</p>
<Button variant="primary" onClick={() => console.log('clicked')}>
操作按钮
</Button>
</div>
);
}
5.2 本地开发热更新
# 根目录执行,同时启动所有应用的 dev 模式
pnpm dev
# 或者只启动特定应用
pnpm --filter @myrepo/web-admin dev
# 添加依赖到特定包
pnpm --filter @myrepo/ui add lodash
# 添加依赖到根目录(共享 devDependencies)
pnpm add -D typescript -w
六、版本管理与发布
6.1 Changeset 工作流
# 安装
pnpm add -D @changesets/cli -w
# 初始化
pnpm changeset init
# 开发完功能后,添加变更集
pnpm changeset
# 选择要发布的包
# 填写变更描述(支持 minor/major/patch)
# 版本号自动更新 + 生成 CHANGELOG
pnpm version-packages
# 构建并发布到 npm
pnpm release
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myrepo/web-admin", "@myrepo/web-portal"]
}
七、CI/CD 集成
# .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
with:
fetch-depth: 2 # Turborepo 需要 git 历史
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Setup Turborepo cache
uses: dtinth/setup-github-actions-caching-for-turbo@v1
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
- name: Build
run: pnpm build
# 只构建变更的应用
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install
run: pnpm install
- name: Build affected
run: |
# 只构建受影响的项目
npx turbo run build --filter=[HEAD^1]
八、性能优化:Turborepo 远程缓存
# 登录 Vercel 远程缓存
npx turbo login
# 链接到远程缓存
npx turbo link
# 后续构建会自动上传/下载缓存
# 团队成员共享构建结果,大幅提升 CI 速度
九、常见问题与解决方案
问题 1:循环依赖
# 检测循环依赖
pnpm m ls --json | grep -A5 "cycles"
问题 2:类型定义不同步
// packages/ui/tsconfig.json
{
"extends": "@myrepo/tsconfig/base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true // 生成源映射,跳转更准确
}
}
问题 3:Husky 在 Monorepo 中的配置
// package.json (根目录)
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
十、总结
Monorepo 不是银弹,但在以下场景收益巨大:
- 多个项目共享组件库/工具函数
- 需要原子提交保证多包一致性
- 团队规模 10+ 人,需要统一工程规范
迁移建议: 小团队先用 pnpm workspace,需要任务编排时引入 Turborepo,稳定后再上 Changeset 版本管理。
🚀 真实收益数据: 某 30 人前端团队迁移 Monorepo 后,组件库发布频率从每月 1 次提升到每周 3 次,重复代码减少 60%,新人 onboarding 时间从 3 天缩短到 2 小时。