Skip to content

Monorepo 课程计划

课程概述

本课程将带你从零开始实现一个 Monorepo 管理工具。

课程目标

  • 理解 Monorepo 的概念
  • 掌握包管理和工作区
  • 学习任务执行和依赖管理
  • 能够构建自己的 Monorepo 工具

课程结构

8 节课,每节 20-40 分钟,总时长约 90-120 分钟。


第 1 课:Monorepo 简介

目标

  • 了解 Monorepo 的背景
  • 理解 Monorepo 的优势
  • 掌握 Monorepo 的核心概念

内容

  1. Monorepo 背景

    • 什么是 Monorepo
    • 为什么需要 Monorepo
    • Monorepo vs Multirepo
  2. 核心概念

    • 工作区(Workspace)
    • 包(Package)
    • 依赖管理
  3. 常用工具

    • Lerna
    • Nx
    • Turborepo
    • pnpm workspaces

实践步骤

bash
# 1. 创建项目
mkdir monorepo-course
cd monorepo-course

# 2. 初始化项目
npm init -y

# 3. 配置工作区
echo '"workspaces": ["packages/*"]' >> package.json

预期输出

✓ 项目初始化完成
✓ 工作区配置完成

总结

  • ✅ 理解了 Monorepo 的概念
  • ✅ 掌握了核心概念
  • ✅ 了解了常用工具

第 2 课:包发现

目标

  • 理解包发现的原理
  • 实现包发现器
  • 学习包配置

内容

  1. 包发现

    • 什么是包
    • 包的识别
    • 包的配置
  2. 实现包发现

    • 递归查找
    • package.json 解析
    • 包过滤
  3. 测试包发现

    • 单元测试
    • 边界测试

实践步骤

bash
# 1. 创建包发现器
cat > packages/tools/discover.js << 'EOF'
import { readFileSync, readdirSync, existsSync } from 'fs'
import { join, dirname } from 'path'

export function discoverPackages(root) {
  const packages = []
  
  function traverse(dir) {
    const packageJsonPath = join(dir, 'package.json')
    
    if (existsSync(packageJsonPath)) {
      const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
      packages.push({
        name: packageJson.name,
        version: packageJson.version,
        path: dir,
        dependencies: packageJson.dependencies || {},
        devDependencies: packageJson.devDependencies || {}
      })
    }
    
    // 继续遍历子目录
    const entries = readdirSync(dir)
    for (const entry of entries) {
      const fullPath = join(dir, entry)
      const stat = require('fs').statSync(fullPath)
      
      if (stat.isDirectory() && entry !== 'node_modules') {
        traverse(fullPath)
      }
    }
  }
  
  traverse(root)
  return packages
}
EOF

# 2. 创建测试
cat > test/discover.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { discoverPackages } from '../packages/tools/discover.js'

describe('包发现测试', () => {
  it('应该发现包', () => {
    const packages = discoverPackages(process.cwd())
    assert.ok(Array.isArray(packages))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该发现包

总结

  • ✅ 实现了包发现
  • ✅ 理解了包配置
  • ✅ 掌握了递归查找

第 3 课:依赖分析

目标

  • 理解依赖分析的原理
  • 实现依赖分析器
  • 学习依赖图构建

内容

  1. 依赖分析

    • 什么是依赖分析
    • 依赖类型
    • 依赖冲突
  2. 实现依赖分析

    • 构建依赖图
    • 检测循环依赖
    • 检测重复依赖
  3. 测试依赖分析

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建依赖分析器
cat > packages/tools/analyze.js << 'EOF'
export function analyzeDependencies(packages) {
  const graph = new Map()
  
  for (const pkg of packages) {
    const deps = new Set()
    
    // 分析生产依赖
    for (const [name, version] of Object.entries(pkg.dependencies)) {
      deps.add(name)
    }
    
    // 分析开发依赖
    for (const [name, version] of Object.entries(pkg.devDependencies)) {
      deps.add(name)
    }
    
    graph.set(pkg.name, Array.from(deps))
  }
  
  return graph
}
EOF

# 2. 创建测试
cat > test/analyze.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { analyzeDependencies } from '../packages/tools/analyze.js'

describe('依赖分析测试', () => {
  it('应该分析依赖', () => {
    const packages = [
      { name: 'a', dependencies: { b: '^1.0.0' } }
    ]
    const graph = analyzeDependencies(packages)
    assert.ok(graph.has('a'))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该分析依赖

总结

  • ✅ 实现了依赖分析
  • ✅ 理解了依赖图
  • ✅ 掌握了依赖冲突检测

第 4 课:拓扑排序

目标

  • 理解拓扑排序的原理
  • 实现拓扑排序算法
  • 学习执行顺序确定

内容

  1. 拓扑排序

    • 什么是拓扑排序
    • 拓扑排序算法
    • 循环依赖处理
  2. 实现拓扑排序

    • DFS 实现
    • Kahn 算法
    • 并行执行
  3. 测试拓扑排序

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建拓扑排序
cat > packages/tools/toposort.js << 'EOF'
export function topologicalSort(graph) {
  const visited = new Set()
  const result = []
  
  function visit(node) {
    if (visited.has(node)) return
    
    visited.add(node)
    
    const deps = graph.get(node) || []
    for (const dep of deps) {
      visit(dep)
    }
    
    result.push(node)
  }
  
  for (const node of graph.keys()) {
    visit(node)
  }
  
  return result
}
EOF

# 2. 创建测试
cat > test/toposort.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { topologicalSort } from '../packages/tools/toposort.js'

describe('拓扑排序测试', () => {
  it('应该进行拓扑排序', () => {
    const graph = new Map([
      ['a', ['b']],
      ['b', []]
    ])
    const result = topologicalSort(graph)
    assert.ok(result.indexOf('b') < result.indexOf('a'))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该进行拓扑排序

总结

  • ✅ 实现了拓扑排序
  • ✅ 理解了排序算法
  • ✅ 掌握了执行顺序确定

第 5 课:任务执行

目标

  • 理解任务执行的原理
  • 实现任务执行器
  • 学习并行和串行执行

内容

  1. 任务执行

    • 什么是任务执行
    • 执行策略
    • 错误处理
  2. 实现任务执行

    • 串行执行
    • 并行执行
    • 过滤执行
  3. 测试任务执行

    • 单元测试
    • 集成测试

实践步骤

bash
# 1. 创建任务执行器
cat > packages/tools/runner.js << 'EOF'
import { exec } from 'child_process'
import { promisify } from 'util'

const execAsync = promisify(exec)

export async function runTask(packages, task, options = {}) {
  const { parallel = false, scope } = options
  
  let filteredPackages = packages
  
  if (scope) {
    filteredPackages = packages.filter(pkg => pkg.name === scope)
  }
  
  if (parallel) {
    return Promise.all(filteredPackages.map(pkg => runPackageTask(pkg, task)))
  } else {
    for (const pkg of filteredPackages) {
      await runPackageTask(pkg, task)
    }
  }
}

async function runPackageTask(pkg, task) {
  console.log(`Running ${task} in ${pkg.name}`)
  await execAsync(`cd ${pkg.path} && npm run ${task}`)
}
EOF

# 2. 创建测试
cat > test/runner.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { runTask } from '../packages/tools/runner.js'

describe('任务执行测试', () => {
  it('应该执行任务', async () => {
    const packages = [{ name: 'test', path: '.' }]
    await runTask(packages, 'test')
    assert.ok(true)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该执行任务

总结

  • ✅ 实现了任务执行
  • ✅ 理解了执行策略
  • ✅ 掌握了错误处理

第 6 课:版本管理

目标

  • 理解版本管理的原理
  • 实现版本管理器
  • 学习语义化版本

内容

  1. 版本管理

    • 什么是语义化版本
    • 版本范围
    • 版本冲突
  2. 实现版本管理

    • 版本解析
    • 版本比较
    • 版本升级
  3. 测试版本管理

    • 单元测试
    • 版本测试

实践步骤

bash
# 1. 创建版本管理器
cat > packages/tools/version.js << 'EOF'
export function parseVersion(version) {
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/)
  if (!match) return null
  
  return {
    major: parseInt(match[1]),
    minor: parseInt(match[2]),
    patch: parseInt(match[3]),
    prerelease: match[4] || null
  }
}

export function compareVersions(v1, v2) {
  const parsed1 = parseVersion(v1)
  const parsed2 = parseVersion(v2)
  
  if (parsed1.major !== parsed2.major) {
    return parsed1.major - parsed2.major
  }
  if (parsed1.minor !== parsed2.minor) {
    return parsed1.minor - parsed2.minor
  }
  if (parsed1.patch !== parsed2.patch) {
    return parsed1.patch - parsed2.patch
  }
  
  return 0
}
EOF

# 2. 创建测试
cat > test/version.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parseVersion, compareVersions } from '../packages/tools/version.js'

describe('版本管理测试', () => {
  it('应该解析版本', () => {
    const version = parseVersion('1.2.3')
    assert.strictEqual(version.major, 1)
    assert.strictEqual(version.minor, 2)
    assert.strictEqual(version.patch, 3)
  })
  
  it('应该比较版本', () => {
    const result = compareVersions('1.2.3', '1.2.4')
    assert.ok(result < 0)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该解析版本
✓ 应该比较版本

总结

  • ✅ 实现了版本管理
  • ✅ 理解了语义化版本
  • ✅ 掌握了版本比较

第 7 课:依赖链接

目标

  • 理解依赖链接的原理
  • 实现依赖链接器
  • 学习符号链接

内容

  1. 依赖链接

    • 什么是依赖链接
    • 链接策略
    • 符号链接
  2. 实现依赖链接

    • 创建符号链接
    • 更新 node_modules
    • 清理链接
  3. 测试依赖链接

    • 单元测试
    • 集成测试

实践步骤

bash
# 1. 创建依赖链接器
cat > packages/tools/link.js << 'EOF'
import { symlink, mkdir } from 'fs/promises'
import { join, dirname } from 'path'

export async function linkDependencies(packages) {
  for (const pkg of packages) {
    const nodeModulesPath = join(pkg.path, 'node_modules')
    
    try {
      await mkdir(nodeModulesPath, { recursive: true })
    } catch (error) {
      // 目录已存在
    }
    
    // 链接本地依赖
    for (const depName of Object.keys(pkg.dependencies)) {
      const depPkg = packages.find(p => p.name === depName)
      
      if (depPkg) {
        const linkPath = join(nodeModulesPath, depName)
        const targetPath = depPkg.path
        
        try {
          await symlink(targetPath, linkPath, 'dir')
          console.log(`Linked ${depName} -> ${targetPath}`)
        } catch (error) {
          // 链接已存在
        }
      }
    }
  }
}
EOF

# 2. 创建测试
cat > test/link.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { linkDependencies } from '../packages/tools/link.js'

describe('依赖链接测试', () => {
  it('应该链接依赖', async () => {
    const packages = []
    await linkDependencies(packages)
    assert.ok(true)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该链接依赖

总结

  • ✅ 实现了依赖链接
  • ✅ 理解了符号链接
  • ✅ 掌握了链接策略

第 8 课:总结与扩展

目标

  • 总结所有课程
  • 回顾关键概念
  • 提供扩展建议

内容

  1. 课程总结

    • 回顾所有功能
    • 总结关键概念
    • 展示完整代码
  2. 扩展建议

    • 添加更多功能
    • 优化性能
    • 支持更多特性
  3. 下一步

    • 学习 Lerna 源码
    • 参与开源项目
    • 构建自己的工具

总结

通过本课程,你学会了:

  1. ✅ Monorepo 概念
  2. ✅ 包发现
  3. ✅ 依赖分析
  4. ✅ 拓扑排序
  5. ✅ 任务执行
  6. ✅ 版本管理
  7. ✅ 依赖链接

下一步

  • 学习 Lerna 源码
  • 添加更多功能
  • 参与开源项目

参考资源

架构师AI杜公众号二维码

扫描二维码关注"架构师AI杜"公众号,获取更多技术内容和最新动态