Appearance
Rollup 课程计划
课程概述
本课程将带你从零开始实现一个模块打包器,理解 Rollup 的核心原理。
课程目标
- 理解模块打包的原理
- 掌握模块解析和依赖分析
- 学习 Tree-shaking 和代码生成
- 能够构建自己的打包工具
课程结构
8 节课,每节 20-40 分钟,总时长约 90-120 分钟。
第 1 课:Rollup 简介
目标
- 了解 Rollup 的背景
- 理解模块打包的概念
- 掌握 Rollup 的核心功能
内容
Rollup 背景
- 什么是 Rollup
- 为什么需要打包器
- Rollup 的历史
核心概念
- 模块
- 模块图
- Tree-shaking
Rollup 的优势
- 更小的输出
- 更好的性能
- 更清晰的代码
实践步骤
bash
# 1. 创建项目
mkdir rollup-course
cd rollup-course
# 2. 初始化项目
npm init -y
# 3. 安装 Rollup
npm install rollup --save-dev预期输出
✓ 项目初始化完成
✓ Rollup 安装完成总结
- ✅ 理解了模块打包的概念
- ✅ 掌握了核心功能
- ✅ 了解了 Rollup 的优势
第 2 课:模块解析
目标
- 理解模块解析的原理
- 实现模块解析器
- 学习路径解析
内容
模块解析
- 什么是模块解析
- 路径解析
- 模块查找
实现模块解析
- 相对路径
- 绝对路径
- node_modules
测试模块解析
- 单元测试
- 边界测试
实践步骤
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 课:模块图构建
目标
- 理解模块图的原理
- 实现模块图构建器
- 学习依赖分析
内容
模块图
- 什么是模块图
- 模块节点
- 依赖边
实现模块图
- 图构建
- 依赖分析
- 循环检测
测试模块图
- 单元测试
- 复杂测试
实践步骤
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 课:代码生成
目标
- 理解代码生成的原理
- 实现代码生成器
- 学习模块包装
内容
代码生成
- 什么是代码生成
- 生成策略
- 模块格式
实现代码生成
- IIFE 格式
- ESM 格式
- CJS 格式
测试代码生成
- 单元测试
- 格式测试
实践步骤
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
- 学习代码优化
内容
Tree-shaking
- 什么是 Tree-shaking
- 原理分析
- 优化效果
实现 Tree-shaking
- 标记使用
- 移除未使用
- 优化代码
测试 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 课:插件系统
目标
- 理解插件系统的设计
- 实现插件接口
- 创建示例插件
内容
插件系统
- 什么是插件
- 插件钩子
- 插件生命周期
实现插件
- 插件接口
- 钩子执行
- 插件配置
创建示例插件
- 替换插件
- 解析插件
- 生成插件
实践步骤
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 生成
- 学习调试支持
内容
Source Map
- 什么是 Source Map
- Source Map 格式
- Source Map 用途
实现 Source Map
- 映射生成
- VLQ 编码
- 文件输出
测试 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 课:总结与扩展
目标
- 总结所有课程
- 回顾关键概念
- 提供扩展建议
内容
课程总结
- 回顾所有功能
- 总结关键概念
- 展示完整代码
扩展建议
- 添加更多功能
- 优化性能
- 支持更多特性
下一步
- 学习 Rollup 源码
- 参与开源项目
- 构建自己的打包工具
总结
通过本课程,你学会了:
- ✅ Rollup 简介
- ✅ 模块解析
- ✅ 模块图构建
- ✅ 代码生成
- ✅ Tree-shaking
- ✅ 插件系统
- ✅ Source Map
下一步
- 学习 Rollup 源码
- 添加更多功能
- 参与开源项目
参考资源

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