Appearance
Monorepo 课程计划
课程概述
本课程将带你从零开始实现一个 Monorepo 管理工具。
课程目标
- 理解 Monorepo 的概念
- 掌握包管理和工作区
- 学习任务执行和依赖管理
- 能够构建自己的 Monorepo 工具
课程结构
8 节课,每节 20-40 分钟,总时长约 90-120 分钟。
第 1 课:Monorepo 简介
目标
- 了解 Monorepo 的背景
- 理解 Monorepo 的优势
- 掌握 Monorepo 的核心概念
内容
Monorepo 背景
- 什么是 Monorepo
- 为什么需要 Monorepo
- Monorepo vs Multirepo
核心概念
- 工作区(Workspace)
- 包(Package)
- 依赖管理
常用工具
- 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 课:包发现
目标
- 理解包发现的原理
- 实现包发现器
- 学习包配置
内容
包发现
- 什么是包
- 包的识别
- 包的配置
实现包发现
- 递归查找
- package.json 解析
- 包过滤
测试包发现
- 单元测试
- 边界测试
实践步骤
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 课:依赖分析
目标
- 理解依赖分析的原理
- 实现依赖分析器
- 学习依赖图构建
内容
依赖分析
- 什么是依赖分析
- 依赖类型
- 依赖冲突
实现依赖分析
- 构建依赖图
- 检测循环依赖
- 检测重复依赖
测试依赖分析
- 单元测试
- 复杂测试
实践步骤
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 课:拓扑排序
目标
- 理解拓扑排序的原理
- 实现拓扑排序算法
- 学习执行顺序确定
内容
拓扑排序
- 什么是拓扑排序
- 拓扑排序算法
- 循环依赖处理
实现拓扑排序
- DFS 实现
- Kahn 算法
- 并行执行
测试拓扑排序
- 单元测试
- 复杂测试
实践步骤
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 课:任务执行
目标
- 理解任务执行的原理
- 实现任务执行器
- 学习并行和串行执行
内容
任务执行
- 什么是任务执行
- 执行策略
- 错误处理
实现任务执行
- 串行执行
- 并行执行
- 过滤执行
测试任务执行
- 单元测试
- 集成测试
实践步骤
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 课:版本管理
目标
- 理解版本管理的原理
- 实现版本管理器
- 学习语义化版本
内容
版本管理
- 什么是语义化版本
- 版本范围
- 版本冲突
实现版本管理
- 版本解析
- 版本比较
- 版本升级
测试版本管理
- 单元测试
- 版本测试
实践步骤
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 课:依赖链接
目标
- 理解依赖链接的原理
- 实现依赖链接器
- 学习符号链接
内容
依赖链接
- 什么是依赖链接
- 链接策略
- 符号链接
实现依赖链接
- 创建符号链接
- 更新 node_modules
- 清理链接
测试依赖链接
- 单元测试
- 集成测试
实践步骤
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 课:总结与扩展
目标
- 总结所有课程
- 回顾关键概念
- 提供扩展建议
内容
课程总结
- 回顾所有功能
- 总结关键概念
- 展示完整代码
扩展建议
- 添加更多功能
- 优化性能
- 支持更多特性
下一步
- 学习 Lerna 源码
- 参与开源项目
- 构建自己的工具
总结
通过本课程,你学会了:
- ✅ Monorepo 概念
- ✅ 包发现
- ✅ 依赖分析
- ✅ 拓扑排序
- ✅ 任务执行
- ✅ 版本管理
- ✅ 依赖链接
下一步
- 学习 Lerna 源码
- 添加更多功能
- 参与开源项目
参考资源

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