Skip to content

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
// 优势:统一版本、代码共享、原子提交

主要优势

  1. 代码共享:轻松共享组件、工具、类型定义
  2. 统一版本:所有包使用相同的依赖版本
  3. 原子提交:跨包的修改可以在一次提交中完成
  4. 统一工具链:共享构建、测试、lint 配置
  5. 更好的协作:团队可见所有代码

主要挑战

  1. 仓库体积:随着项目增多会变得很大
  2. 构建时间:需要智能的增量构建
  3. 权限管理:难以细粒度控制访问权限
  4. 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]" build

5. 如何配置 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 的核心特性是什么?

核心特性

  1. 增量构建:只构建变更的包
  2. 远程缓存:团队共享构建缓存
  3. 并行执行:智能并行任务
  4. 任务管道:定义任务依赖关系

配置文件

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. 当前包的 test

Nx

9. Nx 相比其他工具有什么优势?

核心优势

  1. 智能构建:精确的依赖图分析
  2. 可视化:依赖关系可视化
  3. 代码生成器:快速创建新包
  4. 插件生态:丰富的官方插件

配置文件

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-lib

10. 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 publish
markdown
// .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=3

3. 远程缓存

bash
# Turborepo
turbo login
turbo link

# Nx Cloud
nx connect-to-nx-cloud

4. 优化依赖安装

yaml
# .npmrc
# 使用 pnpm 的严格模式
node-linker=hoisted
shamefully-hoist=false

# 启用缓存
store-dir=~/.pnpm-store

5. TypeScript 项目引用

json
// tsconfig.json
{
  "references": [
    { "path": "./packages/ui" },
    { "path": "./packages/utils" }
  ]
}

// 使用增量编译
tsc --build --incremental

17. 如何处理 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

# 解决方案:重构代码结构
# 将共享代码提取到独立包