Skip to content

Rollup 课程计划

课程概述

本课程将带你从零开始实现一个模块打包器,理解 Rollup 的核心原理。

课程目标

  • 理解模块打包的原理
  • 掌握模块解析和依赖分析
  • 学习 Tree-shaking 和代码生成
  • 能够构建自己的打包工具

课程结构

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


第 1 课:Rollup 简介

目标

  • 了解 Rollup 的背景
  • 理解模块打包的概念
  • 掌握 Rollup 的核心功能

内容

  1. Rollup 背景

    • 什么是 Rollup
    • 为什么需要打包器
    • Rollup 的历史
  2. 核心概念

    • 模块
    • 模块图
    • Tree-shaking
  3. Rollup 的优势

    • 更小的输出
    • 更好的性能
    • 更清晰的代码

实践步骤

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

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

# 3. 安装 Rollup
npm install rollup --save-dev

预期输出

✓ 项目初始化完成
✓ Rollup 安装完成

总结

  • ✅ 理解了模块打包的概念
  • ✅ 掌握了核心功能
  • ✅ 了解了 Rollup 的优势

第 2 课:模块解析

目标

  • 理解模块解析的原理
  • 实现模块解析器
  • 学习路径解析

内容

  1. 模块解析

    • 什么是模块解析
    • 路径解析
    • 模块查找
  2. 实现模块解析

    • 相对路径
    • 绝对路径
    • node_modules
  3. 测试模块解析

    • 单元测试
    • 边界测试

实践步骤

bash
# 1. 创建模块解析器
cat > src/resolver.js << 'EOF'
import { resolve, dirname, join } from 'path'
import { readFileSync } from 'fs'

export function resolveModule(source, importer) {
  // 解析相对路径
  if (source.startsWith('./') || source.startsWith('../')) {
    const resolved = resolve(dirname(importer), source)
    return resolveWithExtensions(resolved)
  }
  
  // 解析 node_modules
  return resolveNodeModules(source, importer)
}

function resolveWithExtensions(basePath) {
  const extensions = ['.js', '.mjs', '.json']
  
  for (const ext of extensions) {
    const path = basePath + ext
    try {
      readFileSync(path)
      return path
    } catch (error) {
      // 文件不存在
    }
  }
  
  return basePath
}

function resolveNodeModules(source, importer) {
  let current = dirname(importer)
  
  while (current !== '/') {
    const nodeModules = join(current, 'node_modules', source)
    const resolved = resolveWithExtensions(nodeModules)
    
    try {
      readFileSync(resolved)
      return resolved
    } catch (error) {
      // 文件不存在
    }
    
    current = dirname(current)
  }
  
  return null
}
EOF

# 2. 创建测试
cat > test/resolver.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { resolveModule } from '../src/resolver.js'

describe('模块解析测试', () => {
  it('应该解析模块', () => {
    const resolved = resolveModule('./test.js', '/path/to/file.js')
    assert.ok(resolved)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该解析模块

总结

  • ✅ 实现了模块解析
  • ✅ 理解了路径解析
  • ✅ 掌握了模块查找

第 3 课:模块图构建

目标

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

内容

  1. 模块图

    • 什么是模块图
    • 模块节点
    • 依赖边
  2. 实现模块图

    • 图构建
    • 依赖分析
    • 循环检测
  3. 测试模块图

    • 单元测试
    • 复杂测试

实践步骤

bash
# 1. 创建模块图
cat > src/graph.js << 'EOF'
import { readFileSync } from 'fs'
import { resolveModule } from './resolver.js'

export function buildModuleGraph(entry) {
  const graph = new Map()
  const visited = new Set()
  
  function visit(moduleId) {
    if (visited.has(moduleId)) return
    
    visited.add(moduleId)
    const module = loadModule(moduleId)
    graph.set(moduleId, module)
    
    for (const dep of module.dependencies) {
      visit(dep)
    }
  }
  
  visit(entry)
  return graph
}

function loadModule(moduleId) {
  const source = readFileSync(moduleId, 'utf-8')
  const dependencies = extractDependencies(source, moduleId)
  
  return {
    id: moduleId,
    source,
    dependencies,
    exports: extractExports(source)
  }
}

function extractDependencies(source, importer) {
  const deps = []
  const importRegex = /import\s+.*?from\s+['"](.*?)['"]/g
  let match
  
  while ((match = importRegex.exec(source)) !== null) {
    const resolved = resolveModule(match[1], importer)
    if (resolved) {
      deps.push(resolved)
    }
  }
  
  return deps
}

function extractExports(source) {
  const exports = []
  const exportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g
  let match
  
  while ((match = exportRegex.exec(source)) !== null) {
    exports.push(match[1])
  }
  
  return exports
}
EOF

# 2. 创建测试
cat > test/graph.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { buildModuleGraph } from '../src/graph.js'

describe('模块图测试', () => {
  it('应该构建模块图', () => {
    const graph = buildModuleGraph('/path/to/entry.js')
    assert.ok(graph instanceof Map)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该构建模块图

总结

  • ✅ 实现了模块图
  • ✅ 理解了依赖分析
  • ✅ 掌握了图构建

第 4 课:代码生成

目标

  • 理解代码生成的原理
  • 实现代码生成器
  • 学习模块包装

内容

  1. 代码生成

    • 什么是代码生成
    • 生成策略
    • 模块格式
  2. 实现代码生成

    • IIFE 格式
    • ESM 格式
    • CJS 格式
  3. 测试代码生成

    • 单元测试
    • 格式测试

实践步骤

bash
# 1. 创建代码生成器
cat > src/generator.js << 'EOF'
export function generateBundle(graph, options = {}) {
  const { format = 'esm' } = options
  
  switch (format) {
    case 'iife':
      return generateIIFE(graph)
    case 'cjs':
      return generateCJS(graph)
    case 'esm':
    default:
      return generateESM(graph)
  }
}

function generateIIFE(graph) {
  let code = '(function () {\n'
  
  for (const [id, module] of graph) {
    code += `  // ${id}\n`
    code += `  ${module.source}\n\n`
  }
  
  code += '})();'
  return code
}

function generateCJS(graph) {
  let code = ''
  
  for (const [id, module] of graph) {
    code += `// ${id}\n`
    code += `${module.source}\n\n`
  }
  
  return code
}

function generateESM(graph) {
  let code = ''
  
  for (const [id, module] of graph) {
    code += `// ${id}\n`
    code += `${module.source}\n\n`
  }
  
  return code
}
EOF

# 2. 创建测试
cat > test/generator.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { generateBundle } from '../src/generator.js'

describe('代码生成器测试', () => {
  it('应该生成 bundle', () => {
    const graph = new Map()
    const code = generateBundle(graph)
    assert.ok(typeof code === 'string')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该生成 bundle

总结

  • ✅ 实现了代码生成
  • ✅ 理解了生成策略
  • ✅ 掌握了模块格式

第 5 课:Tree-shaking

目标

  • 理解 Tree-shaking 的原理
  • 实现 Tree-shaking
  • 学习代码优化

内容

  1. Tree-shaking

    • 什么是 Tree-shaking
    • 原理分析
    • 优化效果
  2. 实现 Tree-shaking

    • 标记使用
    • 移除未使用
    • 优化代码
  3. 测试 Tree-shaking

    • 单元测试
    • 优化测试

实践步骤

bash
# 1. 创建 Tree-shaking
cat > src/treeshake.js << 'EOF'
export function treeShake(graph) {
  const usedExports = new Map()
  
  // 标记入口模块的所有导出为已使用
  const entryModule = Array.from(graph.values())[0]
  for (const exp of entryModule.exports) {
    usedExports.set(entryModule.id, new Set([exp]))
  }
  
  // 递归标记依赖
  markUsedExports(graph, usedExports)
  
  // 移除未使用的代码
  return removeUnusedCode(graph, usedExports)
}

function markUsedExports(graph, usedExports) {
  const visited = new Set()
  
  function visit(moduleId) {
    if (visited.has(moduleId)) return
    
    visited.add(moduleId)
    const module = graph.get(moduleId)
    const used = usedExports.get(moduleId) || new Set()
    
    // 分析模块中使用的导出
    for (const dep of module.dependencies) {
      const usedInDep = analyzeUsage(module.source, dep)
      usedExports.set(dep, usedInDep)
      visit(dep)
    }
  }
  
  for (const moduleId of graph.keys()) {
    visit(moduleId)
  }
}

function analyzeUsage(source, depId) {
  // 分析源代码中对依赖的使用
  // 简化实现
  return new Set()
}

function removeUnusedCode(graph, usedExports) {
  const newGraph = new Map()
  
  for (const [id, module] of graph) {
    const used = usedExports.get(id) || new Set()
    const newSource = removeUnusedExports(module.source, used)
    
    newGraph.set(id, {
      ...module,
      source: newSource
    })
  }
  
  return newGraph
}

function removeUnusedExports(source, usedExports) {
  // 移除未使用的导出
  // 简化实现
  return source
}
EOF

# 2. 创建测试
cat > test/treeshake.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { treeShake } from '../src/treeshake.js'

describe('Tree-shaking 测试', () => {
  it('应该执行 Tree-shaking', () => {
    const graph = new Map()
    const result = treeShake(graph)
    assert.ok(result instanceof Map)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该执行 Tree-shaking

总结

  • ✅ 实现了 Tree-shaking
  • ✅ 理解了优化原理
  • ✅ 掌握了代码优化

第 6 课:插件系统

目标

  • 理解插件系统的设计
  • 实现插件接口
  • 创建示例插件

内容

  1. 插件系统

    • 什么是插件
    • 插件钩子
    • 插件生命周期
  2. 实现插件

    • 插件接口
    • 钩子执行
    • 插件配置
  3. 创建示例插件

    • 替换插件
    • 解析插件
    • 生成插件

实践步骤

bash
# 1. 创建插件系统
cat > src/plugin.js << 'EOF'
export class PluginContext {
  constructor(options) {
    this.options = options
    this.plugins = options.plugins || []
  }
  
  async runHook(hookName, ...args) {
    for (const plugin of this.plugins) {
      if (plugin[hookName]) {
        await plugin[hookName].call(this, ...args)
      }
    }
  }
}

export function createPlugin(options) {
  return {
    name: options.name || 'anonymous-plugin',
    ...options
  }
}
EOF

# 2. 创建测试
cat > test/plugin.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { PluginContext, createPlugin } from '../src/plugin.js'

describe('插件测试', () => {
  it('应该创建插件', () => {
    const plugin = createPlugin({ name: 'test' })
    assert.strictEqual(plugin.name, 'test')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建插件

总结

  • ✅ 实现了插件系统
  • ✅ 理解了插件设计
  • ✅ 掌握了钩子执行

第 7 课:Source Map

目标

  • 理解 Source Map 的原理
  • 实现 Source Map 生成
  • 学习调试支持

内容

  1. Source Map

    • 什么是 Source Map
    • Source Map 格式
    • Source Map 用途
  2. 实现 Source Map

    • 映射生成
    • VLQ 编码
    • 文件输出
  3. 测试 Source Map

    • 单元测试
    • 映射测试

实践步骤

bash
# 1. 创建 Source Map
cat > src/sourcemap.js << 'EOF'
export function generateSourceMap(graph, options = {}) {
  const mappings = []
  let generatedLine = 1
  let generatedColumn = 0
  
  for (const [id, module] of graph) {
    const lines = module.source.split('\n')
    
    for (const line of lines) {
      mappings.push({
        generated: { line: generatedLine, column: generatedColumn },
        original: { line: generatedLine, column: 0 },
        source: id,
        name: null
      })
      
      generatedLine++
      generatedColumn = 0
    }
  }
  
  return {
    version: 3,
    sources: Array.from(graph.keys()),
    names: [],
    mappings: encodeMappings(mappings),
    file: options.file || 'bundle.js'
  }
}

function encodeMappings(mappings) {
  // 简化实现
  return mappings.map(m => {
    return `${m.generated.line},${m.generatedColumn},${m.original.line},${m.original.column}`
  }).join(';')
}
EOF

# 2. 创建测试
cat > test/sourcemap.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { generateSourceMap } from '../src/sourcemap.js'

describe('Source Map 测试', () => {
  it('应该生成 Source Map', () => {
    const graph = new Map()
    const sourceMap = generateSourceMap(graph)
    assert.ok(sourceMap)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该生成 Source Map

总结

  • ✅ 实现了 Source Map
  • ✅ 理解了映射生成
  • ✅ 掌握了调试支持

第 8 课:总结与扩展

目标

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

内容

  1. 课程总结

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

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

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

总结

通过本课程,你学会了:

  1. ✅ Rollup 简介
  2. ✅ 模块解析
  3. ✅ 模块图构建
  4. ✅ 代码生成
  5. ✅ Tree-shaking
  6. ✅ 插件系统
  7. ✅ Source Map

下一步

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

参考资源

架构师AI杜公众号二维码

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