Monorepo 面试题
基础概念
1. 什么是 Monorepo?它解决了什么问题?
核心概念:
- Monorepo(单体仓库):在一个代码仓库中管理多个项目/包
- 与之对应的是 Polyrepo(多仓库):每个项目独立一个仓库
解决的问题:
typescript
// Polyrepo 的问题
repo-a/ (依赖 shared-utils@1.0.0)
repo-b/ (依赖 shared-utils@1.2.0)
repo-c/ (依赖 shared-utils@2.0.0)
// 问题:版本不一致、重复代码、难以协同
// Monorepo 的方案
monorepo/
packages/
shared-utils/ // 共享工具库
app-a/ // 应用 A
app-b/ // 应用 B
app-c/ // 应用 C
// 优势:统一版本、代码共享、原子提交主要优势:
- 代码共享:轻松共享组件、工具、类型定义
- 统一版本:所有包使用相同的依赖版本
- 原子提交:跨包的修改可以在一次提交中完成
- 统一工具链:共享构建、测试、lint 配置
- 更好的协作:团队可见所有代码
主要挑战:
- 仓库体积:随着项目增多会变得很大
- 构建时间:需要智能的增量构建
- 权限管理:难以细粒度控制访问权限
- CI/CD 复杂度:需要智能的变更检测
2. Monorepo 的常见方案有哪些?
主流工具对比:
| 工具 | 特点 | 适用场景 |
|---|---|---|
| pnpm workspace | 快速、节省空间、简单 | 中小型项目 |
| Turborepo | 智能缓存、并行构建 | 需要高性能构建 |
| Nx | 功能全面、可视化 | 大型企业项目 |
| Lerna | 老牌工具、发布管理 | 需要发布 npm 包 |
| Rush | 微软出品、严格模式 | 大型项目 |
typescript
// pnpm workspace 配置
// pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
// Turborepo 配置
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
// Nx 配置
// nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test"]
}
}
}
}pnpm Workspace
3. pnpm workspace 如何工作?
基础配置:
yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'目录结构:
monorepo/
├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── packages/
│ ├── ui/
│ │ ├── package.json
│ │ └── src/
│ ├── utils/
│ │ ├── package.json
│ │ └── src/
│ └── types/
│ ├── package.json
│ └── src/
└── apps/
├── web/
│ ├── package.json
│ └── src/
└── admin/
├── package.json
└── src/包依赖配置:
json
// apps/web/package.json
{
"name": "@monorepo/web",
"dependencies": {
"@monorepo/ui": "workspace:*",
"@monorepo/utils": "workspace:^",
"react": "^18.0.0"
}
}
// packages/ui/package.json
{
"name": "@monorepo/ui",
"dependencies": {
"@monorepo/types": "workspace:*",
"react": "^18.0.0"
}
}workspace 协议:
json
{
"dependencies": {
// 使用 workspace 中的最新版本
"@monorepo/ui": "workspace:*",
// 使用兼容的版本
"@monorepo/utils": "workspace:^",
// 使用精确版本
"@monorepo/types": "workspace:~"
}
}4. pnpm 如何管理依赖?
依赖提升策略:
yaml
# .npmrc
# 不提升依赖(推荐)
hoist=false
# 提升所有依赖
hoist=true
# 只提升特定包
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*依赖安装:
bash
# 为所有包安装依赖
pnpm install
# 为特定包安装依赖
pnpm --filter @monorepo/web add react
# 为所有包安装开发依赖
pnpm add -Dw typescript
# 递归运行脚本
pnpm -r build
# 并行运行
pnpm -r --parallel dev过滤器(Filter):
bash
# 只构建 web 应用
pnpm --filter @monorepo/web build
# 构建 web 及其依赖
pnpm --filter @monorepo/web... build
# 构建依赖 ui 的所有包
pnpm --filter ...@monorepo/ui build
# 构建 packages 目录下的所有包
pnpm --filter "./packages/*" build
# 构建变更的包
pnpm --filter "[origin/main]" build5. 如何配置 pnpm workspace 的脚本?
json
// 根目录 package.json
{
"name": "monorepo",
"scripts": {
// 开发
"dev": "pnpm -r --parallel dev",
"dev:web": "pnpm --filter @monorepo/web dev",
// 构建
"build": "pnpm -r build",
"build:web": "pnpm --filter @monorepo/web... build",
// 测试
"test": "pnpm -r test",
"test:changed": "pnpm --filter \"[origin/main]\" test",
// 代码检查
"lint": "pnpm -r lint",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
// 清理
"clean": "pnpm -r clean && rm -rf node_modules",
// 发布
"publish:packages": "pnpm -r --filter \"./packages/*\" publish"
}
}
// packages/ui/package.json
{
"name": "@monorepo/ui",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest",
"lint": "eslint src"
}
}Turborepo
6. Turborepo 的核心特性是什么?
核心特性:
- 增量构建:只构建变更的包
- 远程缓存:团队共享构建缓存
- 并行执行:智能并行任务
- 任务管道:定义任务依赖关系
配置文件:
json
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// 依赖其他包的 build 任务
"dependsOn": ["^build"],
// 缓存输出
"outputs": ["dist/**", ".next/**"],
// 环境变量
"env": ["NODE_ENV"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
},
"globalDependencies": [
"tsconfig.json",
".eslintrc.js"
]
}使用示例:
bash
# 运行构建
turbo run build
# 运行多个任务
turbo run build test lint
# 强制重新构建
turbo run build --force
# 查看执行计划
turbo run build --dry-run
# 过滤特定包
turbo run build --filter=@monorepo/web
# 只构建变更的包
turbo run build --filter="[origin/main]"7. Turborepo 的缓存机制如何工作?
本地缓存:
typescript
// Turborepo 计算任务的哈希值
// 基于以下因素:
// 1. 任务名称
// 2. 文件内容
// 3. 环境变量
// 4. 依赖的任务输出
// 缓存位置
node_modules/.cache/turbo/
// 缓存命中时
✓ build:@monorepo/ui (CACHED)
// 直接恢复输出,不重新构建远程缓存:
json
// turbo.json
{
"remoteCache": {
"signature": true
}
}bash
# 配置远程缓存
turbo login
turbo link
# 使用远程缓存
turbo run build
# 团队成员可以复用缓存缓存配置:
json
{
"pipeline": {
"build": {
"outputs": ["dist/**"],
// 影响缓存的环境变量
"env": ["NODE_ENV", "API_URL"],
// 影响缓存的文件
"inputs": ["src/**/*.ts", "package.json"]
}
}
}8. Turborepo 的任务编排如何配置?
json
// turbo.json
{
"pipeline": {
// 基础构建
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
// 测试依赖构建
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
// 类型检查
"typecheck": {
"dependsOn": ["^build"]
},
// 开发服务器
"dev": {
"cache": false,
"persistent": true
},
// 发布前检查
"prepublish": {
"dependsOn": ["build", "test", "lint"]
}
}
}任务依赖图:
typescript
// ^ 表示依赖其他包的同名任务
"build": {
"dependsOn": ["^build"]
}
// 执行顺序:
// 1. packages/utils build
// 2. packages/ui build (依赖 utils)
// 3. apps/web build (依赖 ui)
// 不带 ^ 表示依赖当前包的其他任务
"test": {
"dependsOn": ["build"]
}
// 执行顺序:
// 1. 当前包的 build
// 2. 当前包的 testNx
9. Nx 相比其他工具有什么优势?
核心优势:
- 智能构建:精确的依赖图分析
- 可视化:依赖关系可视化
- 代码生成器:快速创建新包
- 插件生态:丰富的官方插件
配置文件:
json
// nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": ["!{projectRoot}/**/*.spec.ts"]
}
}
// workspace.json 或 project.json
{
"projects": {
"@monorepo/web": {
"root": "apps/web",
"sourceRoot": "apps/web/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{workspaceRoot}/dist/apps/web"],
"options": {
"outputPath": "dist/apps/web"
}
}
}
}
}
}使用示例:
bash
# 运行任务
nx run web:build
nx build web
# 运行多个任务
nx run-many --target=build --all
# 只运行受影响的项目
nx affected:build
nx affected:test
# 可视化依赖图
nx graph
# 生成新项目
nx generate @nx/react:app my-app
nx generate @nx/react:lib my-lib10. Nx 的受影响检测(Affected)如何工作?
bash
# 检测受影响的项目
nx affected:apps
nx affected:libs
# 只构建受影响的项目
nx affected:build --base=origin/main
# 只测试受影响的项目
nx affected:test --base=origin/main --head=HEAD
# 在 CI 中使用
nx affected:build --base=origin/main~1 --head=origin/main工作原理:
typescript
// 1. Nx 分析依赖图
packages/ui → apps/web
packages/utils → packages/ui → apps/web
// 2. 检测文件变更
git diff origin/main...HEAD
// 变更:packages/ui/src/Button.tsx
// 3. 计算受影响的项目
// packages/ui 变更
// → apps/web 受影响(依赖 ui)
// → packages/utils 不受影响
// 4. 只运行受影响项目的任务
nx affected:build
// 只构建:packages/ui, apps/web配置:
json
// nx.json
{
"affected": {
"defaultBase": "main"
},
"implicitDependencies": {
"package.json": "*",
"tsconfig.json": "*",
".eslintrc.json": "*"
}
}最佳实践
11. Monorepo 的目录结构如何设计?
monorepo/
├── apps/ # 应用
│ ├── web/ # Web 应用
│ ├── admin/ # 管理后台
│ └── mobile/ # 移动端
├── packages/ # 共享包
│ ├── ui/ # UI 组件库
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ ├── config/ # 共享配置
│ └── hooks/ # 共享 Hooks
├── tools/ # 工具脚本
│ ├── scripts/ # 构建脚本
│ └── generators/ # 代码生成器
├── docs/ # 文档
├── .github/ # GitHub 配置
│ └── workflows/ # CI/CD
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.base.json命名规范:
json
// 使用 scope 命名
{
"name": "@monorepo/web",
"name": "@monorepo/ui",
"name": "@monorepo/utils"
}12. 如何配置共享的 TypeScript 配置?
json
// tsconfig.base.json (根目录)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@monorepo/ui": ["./packages/ui/src"],
"@monorepo/utils": ["./packages/utils/src"],
"@monorepo/types": ["./packages/types/src"]
}
}
}
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": ["src"],
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/utils" }
]
}
// packages/ui/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"composite": true,
"outDir": "./dist",
"declarationDir": "./dist/types"
},
"include": ["src"],
"references": [
{ "path": "../types" }
]
}13. 如何配置共享的 ESLint 和 Prettier?
javascript
// packages/config/eslint-config/index.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'react/react-in-jsx-scope': 'off',
},
}
// apps/web/.eslintrc.js
module.exports = {
extends: ['@monorepo/eslint-config'],
parserOptions: {
project: './tsconfig.json',
},
}
// prettier.config.js (根目录)
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'es5',
printWidth: 100,
tabWidth: 2,
}
// package.json
{
"scripts": {
"lint": "pnpm -r lint",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\""
}
}14. Monorepo 的版本管理策略?
固定版本(Fixed):
json
// 所有包使用相同版本
// lerna.json
{
"version": "1.0.0",
"packages": ["packages/*"]
}独立版本(Independent):
json
// 每个包独立版本
// lerna.json
{
"version": "independent",
"packages": ["packages/*"]
}使用 Changesets:
bash
# 安装
pnpm add -Dw @changesets/cli
pnpm changeset init
# 创建 changeset
pnpm changeset
# 选择变更的包和版本类型
# 生成版本和 CHANGELOG
pnpm changeset version
# 发布
pnpm changeset publishmarkdown
// .changeset/config.json
{
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main"
}15. Monorepo 的 CI/CD 如何配置?
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 获取完整历史用于 affected
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
# 使用 Turborepo
- name: Build
run: pnpm turbo run build --filter="[origin/main]"
- name: Test
run: pnpm turbo run test --filter="[origin/main]"
- name: Lint
run: pnpm turbo run lint --filter="[origin/main]"
# 或使用 Nx
- name: Build (Nx)
run: pnpm nx affected:build --base=origin/main
- name: Test (Nx)
run: pnpm nx affected:test --base=origin/main缓存配置:
yaml
# Turborepo 远程缓存
- name: Setup Turborepo cache
uses: actions/cache@v3
with:
path: node_modules/.cache/turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
# Nx 缓存
- name: Setup Nx cache
uses: actions/cache@v3
with:
path: node_modules/.cache/nx
key: ${{ runner.os }}-nx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nx-性能优化
16. 如何优化 Monorepo 的构建性能?
1. 使用增量构建:
json
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
// 只构建变更的文件
"inputs": ["src/**/*.ts", "package.json"]
}
}
}2. 并行执行:
bash
# Turborepo 自动并行
turbo run build
# pnpm 并行
pnpm -r --parallel build
# Nx 并行
nx run-many --target=build --all --parallel=33. 远程缓存:
bash
# Turborepo
turbo login
turbo link
# Nx Cloud
nx connect-to-nx-cloud4. 优化依赖安装:
yaml
# .npmrc
# 使用 pnpm 的严格模式
node-linker=hoisted
shamefully-hoist=false
# 启用缓存
store-dir=~/.pnpm-store5. TypeScript 项目引用:
json
// tsconfig.json
{
"references": [
{ "path": "./packages/ui" },
{ "path": "./packages/utils" }
]
}
// 使用增量编译
tsc --build --incremental17. 如何处理 Monorepo 中的依赖版本冲突?
1. 统一依赖版本:
json
// 根 package.json
{
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
}
// 子包继承版本
// packages/ui/package.json
{
"devDependencies": {
"typescript": "workspace:*",
"vite": "workspace:*"
}
}2. 使用 syncpack:
bash
# 安装
pnpm add -Dw syncpack
# 检查版本不一致
pnpm syncpack list-mismatches
# 修复版本
pnpm syncpack fix-mismatches
# 配置
# .syncpackrc.json
{
"versionGroups": [
{
"label": "Use workspace protocol",
"dependencies": ["@monorepo/**"],
"dependencyTypes": ["prod", "dev"],
"pinVersion": "workspace:*"
}
]
}3. 使用 overrides:
json
// package.json
{
"pnpm": {
"overrides": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}18. Monorepo 的发布流程如何设计?
使用 Changesets:
bash
# 1. 开发完成后创建 changeset
pnpm changeset
# 2. 选择变更的包
? Which packages would you like to include?
✔ @monorepo/ui
✔ @monorepo/utils
# 3. 选择版本类型
? What kind of change is this for @monorepo/ui?
○ patch
○ minor
● major
# 4. 生成版本和 CHANGELOG
pnpm changeset version
# 5. 提交变更
git add .
git commit -m "chore: version packages"
# 6. 发布
pnpm changeset publish
# 7. 推送 tags
git push --follow-tags自动化发布:
yaml
# .github/workflows/release.yml
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}实战问题
19. 如何在 Monorepo 中共享组件?
typescript
// packages/ui/src/Button.tsx
export const Button = ({ children, ...props }) => {
return <button {...props}>{children}</button>
}
// packages/ui/src/index.ts
export { Button } from './Button'
export { Input } from './Input'
// packages/ui/package.json
{
"name": "@monorepo/ui",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
// apps/web/src/App.tsx
import { Button } from '@monorepo/ui'
function App() {
return <Button>Click me</Button>
}20. Monorepo 的常见问题和解决方案?
问题 1:依赖安装慢
bash
# 解决方案:使用 pnpm
pnpm install # 比 npm/yarn 快 2-3 倍
# 启用缓存
# .npmrc
store-dir=~/.pnpm-store问题 2:构建时间长
bash
# 解决方案:使用 Turborepo 缓存
turbo run build
# 第二次构建几乎瞬间完成
# 或使用 Nx 的智能构建
nx affected:build问题 3:类型检查慢
json
// 解决方案:使用 TypeScript 项目引用
{
"references": [
{ "path": "./packages/ui" }
]
}
// 增量编译
tsc --build --incremental问题 4:IDE 性能问题
json
// tsconfig.json
{
"compilerOptions": {
"incremental": true,
"skipLibCheck": true
},
"exclude": ["**/node_modules", "**/dist"]
}问题 5:循环依赖
bash
# 检测循环依赖
pnpm add -Dw madge
madge --circular --extensions ts,tsx ./packages
# 解决方案:重构代码结构
# 将共享代码提取到独立包