返回笔记首页

Monorepo 架构设计 - 面试完整指南

主题配置

简历描述模板(STAR法则)

模板一:工程化建设视角

主导公司级 Monorepo 架构改造,统一管理 15+ 前端项目,通过 pnpm workspace + Turborepo 将多项目构建时间从 180s 缩短至 45s(提升 75%),包依赖冲突减少 90%,新项目搭建时间从 2 天缩短至 30 分钟。

  • 设计统一的构建配置和代码规范体系,制定组件库、工具库复用机制
  • 引入 Turborepo 增量构建和缓存策略,实现跨项目构建提速
  • 搭建自动化版本发布流程,支持批量发包和 Changelog 自动生成

模板二:团队协作视角

推动团队从多仓库模式迁移至 Monorepo 架构,解决跨项目依赖管理混乱、构建效率低下问题。整合 8 个独立仓库为统一 Monorepo,团队协作效率提升 60%,代码复用率从 30% 提升至 75%。

  • 使用 pnpm workspace 管理多包依赖,解决幽灵依赖和依赖提升问题
  • 配置 Turborepo 管道任务并行执行,构建时间降低 70%
  • 建立统一的 CI/CD 流程,支持按需构建和部署

模板三:业务价值视角

重构前端基础设施,从 Multi-repo 升级为 Monorepo 架构,支撑公司 20+ 业务线前端项目的高效开发。通过统一依赖管理和构建优化,减少 Bug 引入率 45%,新业务线接入成本降低 80%。

  • 设计跨项目依赖治理方案,建立组件库、hooks 库、工具库三层复用体系
  • 实现版本统一管理,避免依赖版本不一致导致的线上问题
  • 配置自动化发布流程,支持单包发布、批量发布和 Lerna 版本管理

面试话术模板

场景一:面试官问"你们为什么要用 Monorepo?"

【核心回答框架】

"我们做的是系统性的工程化升级,而不是为了技术而技术。"

背景(Situation): 我们当时有 15 个前端项目分散在不同仓库,存在三个核心痛点:

  1. 依赖管理混乱:同一个组件库在不同项目有 5 个版本,经常出现 API 不一致导致的线上问题
  2. 协作效率低:跨项目修改需要同时改 3-5 个仓库,发布流程繁琐
  3. 构建时间长:每个项目独立构建,没有缓存复用,CI 流水线耗时 180 秒

任务(Task): 我负责设计并落地 Monorepo 架构改造方案,目标是提升开发效率、统一技术栈、减少维护成本。

行动(Action): 我从"依赖管理、构建优化、协作流程"三个维度切入:

  1. 依赖管理层面
    • 选择 pnpm workspace 而非 yarn/npm:利用硬链接节省磁盘空间 50%,解决幽灵依赖
    • 建立三层依赖结构:shared-ui(组件库)、shared-utils(工具库)、shared-config(配置)
    • 统一版本策略:核心依赖锁定版本,避免依赖地狱
  2. 构建优化层面
    • 引入 Turborepo 增量构建:只构建变更的包,利用缓存加速
    • 配置任务管道:build、test、lint 并行执行
    • 远程缓存:团队共享构建缓存,新同事首次构建也能复用
  3. 协作流程层面
    • 统一脚手架:一键创建新项目,自动注入标准配置
    • 自动化发布:Changeset 管理版本,自动生成 Changelog
    • 代码共享:原子化组件库,业务代码跨项目复用
结果(Result)
  • 构建时间:从 180s 降至 45s(提升 75%)
  • 依赖冲突:版本不一致问题减少 90%
  • 协作效率:跨项目修改从半天缩短到 30 分钟
  • 新项目搭建:从 2 天降至 30 分钟
  • 代码复用率:从 30% 提升到 75%

场景二:面试官追问"为什么选 pnpm 而不是 yarn/npm?"

【对比思维回答】

"我做过详细的技术选型对比,pnpm 在我们的场景下有三个关键优势:"
对比维度 npm/yarn pnpm
磁盘空间 每个项目独立安装 硬链接共享,节省 50%+
幽灵依赖 会提升依赖,可访问未声明的包 严格隔离,避免隐患
安装速度 较慢 快 2-3 倍
Monorepo 支持 workspace 基础功能 原生优化,性能更好

实际案例

  • 我们有个项目依赖了 lodash,但没在 package.json 声明,用 npm 能跑,换个环境就崩了
  • pnpm 的 .pnpm 目录扁平化管理,15 个项目共用依赖,磁盘从 8GB 降到 3GB

代码对比

bash
# npm/yarn 问题
node_modules/
  ├── lodash/         # 可以直接 import,但没声明
  └── your-package/

# pnpm 严格隔离
node_modules/
  ├── .pnpm/          # 所有包在这里
  └── your-package/   # 只能访问声明的依赖

场景三:面试官问"Turborepo 具体怎么优化的?"

【深度技术回答】

"Turborepo 的核心价值是'增量构建 + 任务编排 + 远程缓存',让我展开讲讲:"
1. 任务依赖图(Pipeline)
json
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],  // 依赖包先构建
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "cache": false            // 测试不缓存
    },
    "lint": {}                  // 可并行执行
  }
}

运行机制

  • Turbo 分析依赖关系,构建拓扑排序
  • 并行执行无依赖任务(lint、test 同时跑)
  • 增量构建:只重新构建变更的包
2. 缓存策略
bash
# 本地缓存
.turbo/cache/
  ├── abc123def.tar.gz  # 基于 inputs hash
  └── xyz789uvw.tar.gz

# 远程缓存(Vercel/自建)
turbo run build --cache-dir=.turbo --remote-cache

实际效果

  • 首次构建:180s(所有包)
  • 二次构建(无变更):3s(全命中缓存)
  • 修改一个包:25s(只构建该包及依赖它的包)
3. 任务过滤

bash

bash
# 只构建变更的包
turbo run build --filter=...@origin/main

# 只构建特定包
turbo run build --filter=@myapp/ui

# 并行构建
turbo run build test lint --parallel

数据对比

plain
优化前(串行构建):
├── @app/admin build: 60s
├── @app/mobile build: 50s
└── @app/web build: 70s
总计:180s

优化后(Turbo并行+缓存):
├── @app/admin build: 22s(并行)
├── @app/mobile build: 18s(并行)
└── @app/web build: 25s(并行+缓存命中部分)
总计:45s(最长任务时间)

场景四:面试官问"依赖治理具体怎么做的?"

【方法论回答】

"我建立了三层依赖架构 + 版本管控机制:"

三层架构

plain
packages/
├── shared-ui/          # L1: 基础组件库
│   ├── Button/
│   ├── Table/
│   └── Form/
├── shared-utils/       # L2: 工具函数库
│   ├── request/
│   ├── auth/
│   └── storage/
├── shared-config/      # L3: 配置库
│   ├── eslint-config/
│   ├── tsconfig/
│   └── vite-config/
└── apps/               # 业务应用
    ├── admin/
    ├── mobile/
    └── web/

版本管控

json
// 根目录 package.json
{
  "pnpm": {
    "overrides": {
      "react": "^18.2.0",      // 强制统一版本
      "lodash": "^4.17.21"
    }
  },
  "devDependencies": {
    "@types/react": "^18.2.0"  // 全局共享
  }
}

依赖分析工具

bash
# 检查重复依赖
pnpm list --depth=Infinity | grep "lodash"

# 依赖关系图
pnpm why react

# 清理幽灵依赖
pnpm install --shamefully-hoist=false

治理规则

  1. 核心依赖统一:React、Vue、TypeScript 全局锁版本
  2. 组件库内部依赖:使用 workspace:* 协议
  3. 第三方库升级:统一升级,避免碎片化
  4. Peer Dependencies:明确声明,避免隐式依赖

实际案例

json
// ❌ 问题写法
"dependencies": {
  "@myapp/ui": "1.2.3"  // 硬编码版本
}

// ✅ 正确写法
"dependencies": {
  "@myapp/ui": "workspace:*"  // 永远指向本地最新
}

场景五:面试官问"自动化发布是怎么实现的?"

【流程化回答】

"我们用 Changesets 实现自动化发布,整个流程零人工介入:"

发布流程图

plain
开发提交代码
   ↓
执行 pnpm changeset(生成变更记录)
   ↓
PR 合并到 main
   ↓
CI 自动创建 "Version Packages" PR
   ↓
合并后自动发布到 npm
   ↓
自动生成 GitHub Release + Changelog

核心配置

json
// .changeset/config.json
{
  "changelog": "@changesets/changelog-github",
  "commit": true,
  "linked": [],           // 独立版本
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch"
}

开发者使用

bash
# 1. 修改代码后,执行
pnpm changeset

# 2. 选择变更类型
? Which packages would you like to include?
  ✓ @myapp/ui
  ✓ @myapp/utils

? What type of change is this for @myapp/ui?
  ○ patch (bug fix)
  ○ minor (new feature)
  ● major (breaking change)

# 3. 生成 .changeset/xxx.md
---
"@myapp/ui": major
---

重构 Button 组件 API

CI 自动化

yaml
# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    steps:
      - uses: changesets/action@v1
        with:
          publish: pnpm release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

成果数据

  • 发布时间:从 30 分钟降至 5 分钟
  • 错误率:人工发布错误率 15% → 自动化 0%
  • Changelog:自动生成,包含 PR 链接和贡献者

核心代码实现

1. pnpm workspace 配置

pnpm-workspace.yaml

yaml
packages:
  # 所有 packages 下的包
  - 'packages/*'

  # 所有 apps 下的应用
  - 'apps/*'

  # 排除测试目录
  - '!**/test/**'

根目录 package.json

json
{
  "name": "monorepo-root",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "clean": "turbo run clean && rm -rf node_modules",
    "changeset": "changeset",
    "version": "changeset version",
    "release": "pnpm build && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.26.2",
    "turbo": "^1.10.0",
    "typescript": "^5.0.0",
    "eslint": "^8.45.0",
    "prettier": "^3.0.0"
  },
  "pnpm": {
    "overrides": {
      "react": "^18.2.0",
      "react-dom": "^18.2.0"
    },
    "peerDependencyRules": {
      "ignoreMissing": ["react", "react-dom"]
    }
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=8.0.0"
  }
}

2. Turborepo 配置

turbo.json

json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".env",
    "tsconfig.json"
  ],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**"],
      "env": ["NODE_ENV"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "cache": false
    },
    "lint": {
      "outputs": []
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "clean": {
      "cache": false
    }
  },
  "remoteCache": {
    "enabled": true
  }
}

3. 共享配置包

packages/shared-config/eslint-config/index.js

javascript
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier'
  ],
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  settings: {
    react: {
      version: 'detect'
    }
  },
  rules: {
    'no-console': ['warn', { allow: ['warn', 'error'] }],
    '@typescript-eslint/no-explicit-any': 'warn',
    'react/react-in-jsx-scope': 'off'
  }
};

packages/shared-config/tsconfig/base.json

json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowJs": false,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "incremental": true
  },
  "exclude": ["node_modules", "dist", "build"]
}

packages/shared-config/tsconfig/react.json

json
{
  "extends": "./base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["ES2020", "DOM", "DOM.Iterable"]
  }
}

4. 组件库包示例

packages/shared-ui/package.json

json
{
  "name": "@myapp/ui",
  "version": "1.0.0",
  "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"
    },
    "./Button": {
      "import": "./dist/Button.mjs",
      "require": "./dist/Button.js",
      "types": "./dist/Button.d.ts"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@myapp/eslint-config": "workspace:*",
    "@myapp/tsconfig": "workspace:*",
    "@types/react": "^18.2.0",
    "tsup": "^7.2.0",
    "typescript": "^5.0.0"
  }
}

packages/shared-ui/tsup.config.ts

typescript
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts', 'src/Button/index.tsx'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  external: ['react', 'react-dom'],
  treeshake: true,
  minify: process.env.NODE_ENV === 'production'
});

5. 应用包示例

apps/admin/package.json

json
{
  "name": "@myapp/admin",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@myapp/ui": "workspace:*",
    "@myapp/utils": "workspace:*"
  },
  "devDependencies": {
    "@myapp/eslint-config": "workspace:*",
    "@myapp/tsconfig": "workspace:*",
    "@myapp/vite-config": "workspace:*",
    "@vitejs/plugin-react": "^4.0.0",
    "vite": "^4.4.0",
    "typescript": "^5.0.0"
  }
}

apps/admin/tsconfig.json

json

json
{
  "extends": "@myapp/tsconfig/react.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "references": [
    { "path": "../../packages/shared-ui" },
    { "path": "../../packages/shared-utils" }
  ]
}

6. 自动化脚本

scripts/check-deps.js

javascript
#!/usr/bin/env node
/**
 * 检查重复依赖
 */
const { execSync } = require('child_process');

const duplicates = execSync('pnpm list --depth=Infinity --json', {
  encoding: 'utf-8'
});

const parsed = JSON.parse(duplicates);
const depMap = new Map();

function traverse(deps) {
  if (!deps) return;

  Object.entries(deps).forEach(([name, info]) => {
    const key = `${name}@${info.version}`;
    if (!depMap.has(name)) {
      depMap.set(name, new Set());
    }
    depMap.get(name).add(info.version);

    if (info.dependencies) {
      traverse(info.dependencies);
    }
  });
}

traverse(parsed[0]?.dependencies);

console.log('\n🔍 检查重复依赖:\n');
let hasDuplicates = false;

depMap.forEach((versions, name) => {
  if (versions.size > 1) {
    hasDuplicates = true;
    console.log(`⚠️  ${name}: ${[...versions].join(', ')}`);
  }
});

if (!hasDuplicates) {
  console.log('✅ 没有重复依赖');
} else {
  console.log('\n💡 建议在根 package.json 中配置 pnpm.overrides 统一版本');
  process.exit(1);
}

scripts/create-package.js

javascript
#!/usr/bin/env node
/**
 * 快速创建新包
 */
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const packageName = process.argv[2];
const packageType = process.argv[3] || 'lib'; // lib | app

if (!packageName) {
  console.error('❌ 请提供包名: pnpm create:pkg my-package');
  process.exit(1);
}

const isApp = packageType === 'app';
const dir = path.join(
  process.cwd(),
  isApp ? 'apps' : 'packages',
  packageName
);

if (fs.existsSync(dir)) {
  console.error(`❌ 目录已存在: ${dir}`);
  process.exit(1);
}

// 创建目录结构
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });

// 生成 package.json
const pkgJson = {
  name: `@myapp/${packageName}`,
  version: '0.0.1',
  private: isApp,
  main: isApp ? undefined : './dist/index.js',
  types: isApp ? undefined : './dist/index.d.ts',
  scripts: {
    dev: isApp ? 'vite' : 'tsup --watch',
    build: isApp ? 'vite build' : 'tsup',
    lint: 'eslint src/',
    'type-check': 'tsc --noEmit'
  },
  dependencies: {},
  devDependencies: {
    '@myapp/eslint-config': 'workspace:*',
    '@myapp/tsconfig': 'workspace:*',
    typescript: '^5.0.0'
  }
};

fs.writeFileSync(
  path.join(dir, 'package.json'),
  JSON.stringify(pkgJson, null, 2)
);

// 生成 tsconfig.json
const tsconfig = {
  extends: '@myapp/tsconfig/base.json',
  compilerOptions: {
    baseUrl: '.',
    outDir: 'dist'
  },
  include: ['src']
};

fs.writeFileSync(
  path.join(dir, 'tsconfig.json'),
  JSON.stringify(tsconfig, null, 2)
);

// 生成示例文件
const indexTs = isApp
  ? `console.log('Hello from ${packageName}');`
  : `export const hello = () => '${packageName}';`;

fs.writeFileSync(path.join(dir, 'src/index.ts'), indexTs);

console.log(`✅ 创建成功: ${dir}`);
console.log(`\n📦 下一步:`);
console.log(`  cd ${isApp ? 'apps' : 'packages'}/${packageName}`);
console.log(`  pnpm install`);
console.log(`  pnpm dev`);

7. CI/CD 配置

.github/workflows/ci.yml

yaml
name: CI

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - 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

      - name: Lint
        run: pnpm lint

      - name: Type Check
        run: pnpm type-check

      - name: Build
        run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

      - name: Test
        run: pnpm test

  release:
    needs: lint-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Create Release Pull Request or Publish
        uses: changesets/action@v1
        with:
          publish: pnpm release
          commit: 'chore: release packages'
          title: 'chore: release packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

性能对比数据

构建时间对比

场景 Multi-repo Monorepo (无缓存) Monorepo (有缓存)
全量构建 180s 90s 45s
单包构建 60s 25s 3s
增量构建 60s 30s 8s

磁盘空间对比

模式 node_modules 大小 .turbo 缓存 总计
Multi-repo 8.2 GB 0 8.2 GB
Monorepo 3.1 GB 1.5 GB 4.6 GB