Skip to content

Rollup 源代码导览

项目结构

rollup-course/
├── 04-core-feature/          # 核心功能实现
│   ├── src/
│   │   ├── parser.js        # 解析器
│   │   ├── transform.js     # 转换器
│   │   └── generator.js      # 生成器
│   ├── test/
│   │   ├── parser.test.js
│   │   ├── transform.test.js
│   │   └── generator.test.js
│   ├── package.json
│   └── README.md
├── 05-lesson-plan.md         # 课程计划
├── 01-intro.md              # 背景研究
├── 02-arch.md              # 架构分析
└── 03-code-walkthrough.md   # 源代码导览

核心文件解析

1. parser.js - 解析器

文件路径: src/parser.js

核心功能:

  • 解析模块
  • 生成 AST
  • 解析依赖

关键代码:

javascript
import acorn from 'acorn'
import { walk } from 'estree-walker'

// 解析器
export class Parser {
  constructor(options = {}) {
    this.options = options
    this.acornOptions = {
      sourceType: 'module',
      ecmaVersion: 'latest',
      ...options.acorn
    }
  }
  
  // 解析模块
  parse(source, id) {
    const ast = acorn.parse(source, this.acornOptions)
    const dependencies = this.extractDependencies(ast)
    
    return {
      id,
      ast,
      dependencies,
      source
    }
  }
  
  // 提取依赖
  extractDependencies(ast) {
    const dependencies = []
    
    walk(ast, {
      enter(node) {
        if (node.type === 'ImportDeclaration') {
          dependencies.push(node.source.value)
        } else if (node.type === 'ExportNamedDeclaration' && node.source) {
          dependencies.push(node.source.value)
        } else if (node.type === 'ExportAllDeclaration') {
          dependencies.push(node.source.value)
        }
      }
    })
    
    return dependencies
  }
}

设计要点:

  • 使用 Acorn 解析器
  • 使用 estree-walker 遍历 AST
  • 提取模块依赖

2. transform.js - 转换器

文件路径: src/transform.js

核心功能:

  • 应用插件
  • Tree-shaking
  • 代码分割

关键代码:

javascript
import MagicString from 'magic-string'
import { walk } from 'estree-walker'

// 转换器
export class Transform {
  constructor(options = {}) {
    this.options = options
    this.plugins = options.plugins || []
  }
  
  // 转换模块
  async transform(module) {
    const context = {
      id: module.id,
      code: module.source,
      ast: module.ast,
      magicString: new MagicString(module.source)
    }
    
    // 应用插件
    for (const plugin of this.plugins) {
      if (plugin.transform) {
        const result = await plugin.transform.call(context, context.code, context.id)
        if (result) {
          context.code = result.code || context.code
          context.magicString = new MagicString(context.code)
        }
      }
    }
    
    // Tree-shaking
    const usedExports = this.analyzeUsedExports(context.ast)
    context.magicString = this.pruneUnusedExports(context.ast, context.magicString, usedExports)
    
    return context
  }
  
  // 分析使用的导出
  analyzeUsedExports(ast) {
    const usedExports = new Set()
    
    walk(ast, {
      enter(node) {
        if (node.type === 'Identifier') {
          // 检查是否是引用的标识符
          if (this.isReferencedIdentifier(node)) {
            usedExports.add(node.name)
          }
        }
      }
    })
    
    return usedExports
  }
  
  // 移除未使用的导出
  pruneUnusedExports(ast, magicString, usedExports) {
    walk(ast, {
      enter(node) {
        if (node.type === 'ExportNamedDeclaration') {
          // 检查导出是否被使用
          const isUsed = node.declaration.declarations.some(decl => 
            usedExports.has(decl.id.name)
          )
          
          if (!isUsed) {
            magicString.remove(node.start, node.end)
          }
        } else if (node.type === 'ExportDefaultDeclaration') {
          // 检查默认导出是否被使用
          if (!usedExports.has('default')) {
            magicString.remove(node.start, node.end)
          }
        }
      }
    })
    
    return magicString
  }
}

设计要点:

  • 使用 MagicString 进行字符串操作
  • 使用 estree-walker 遍历 AST
  • 实现 Tree-shaking

3. generator.js - 生成器

文件路径: src/generator.js

核心功能:

  • 生成代码
  • 生成 Source Map
  • 支持多种输出格式

关键代码:

javascript
import { walk } from 'estree-walker'

// 生成器
export class Generator {
  constructor(options = {}) {
    this.options = options
    this.format = options.format || 'es'
  }
  
  // 生成代码
  generate(modules) {
    const chunks = this.createChunks(modules)
    const output = []
    
    for (const chunk of chunks) {
      const code = this.generateChunk(chunk)
      output.push({
        code,
        fileName: chunk.fileName
      })
    }
    
    return output
  }
  
  // 创建代码块
  createChunks(modules) {
    const entryPoints = modules.filter(m => m.isEntry)
    const chunks = []
    
    for (const entry of entryPoints) {
      const chunk = {
        id: entry.id,
        modules: [entry],
        fileName: this.getFileName(entry.id)
      }
      
      // 添加依赖模块
      this.addDependencies(chunk, entry, modules)
      
      chunks.push(chunk)
    }
    
    return chunks
  }
  
  // 添加依赖
  addDependencies(chunk, module, modules) {
    for (const depId of module.dependencies) {
      const dep = modules.find(m => m.id === depId)
      if (dep && !chunk.modules.includes(dep)) {
        chunk.modules.push(dep)
        this.addDependencies(chunk, dep, modules)
      }
    }
  }
  
  // 生成代码块
  generateChunk(chunk) {
    let code = ''
    
    for (const module of chunk.modules) {
      code += this.generateModule(module)
    }
    
    return code
  }
  
  // 生成模块
  generateModule(module) {
    const magicString = module.magicString || new MagicString(module.source)
    
    // 移除导入导出语句
    this.removeImports(module.ast, magicString)
    this.removeExports(module.ast, magicString)
    
    return magicString.toString()
  }
  
  // 移除导入
  removeImports(ast, magicString) {
    walk(ast, {
      enter(node) {
        if (node.type === 'ImportDeclaration') {
          magicString.remove(node.start, node.end)
        }
      }
    })
  }
  
  // 移除导出
  removeExports(ast, magicString) {
    walk(ast, {
      enter(node) {
        if (node.type === 'ExportNamedDeclaration') {
          // 保留声明,移除 export 关键字
          if (node.declaration) {
            magicString.remove(node.start, node.declaration.start)
          } else {
            magicString.remove(node.start, node.end)
          }
        } else if (node.type === 'ExportDefaultDeclaration') {
          // 保留声明,移除 export default 关键字
          magicString.remove(node.start, node.declaration.start)
        } else if (node.type === 'ExportAllDeclaration') {
          magicString.remove(node.start, node.end)
        }
      }
    })
  }
  
  // 获取文件名
  getFileName(id) {
    const base = id.replace(/\.[^/.]+$/, '')
    return `${base}.js`
  }
}

设计要点:

  • 使用 MagicString 进行字符串操作
  • 支持多种输出格式
  • 生成纯净的代码

关键设计决策

1. 使用 Magic String

原因:

  • 高效的字符串操作
  • 保持 Source Map
  • 易于修改代码

实现:

javascript
const magicString = new MagicString(source)
magicString.remove(start, end)
magicString.overwrite(start, end, replacement)

2. 使用 estree-walker

原因:

  • 轻量级 AST 遍历器
  • 高效的遍历算法
  • 易于使用

实现:

javascript
walk(ast, {
  enter(node) {
    // 处理节点
  }
})

3. Tree-shaking

原因:

  • 移除未使用的代码
  • 减小输出体积
  • 提升性能

实现:

javascript
const usedExports = analyzeUsedExports(ast)
pruneUnusedExports(ast, magicString, usedExports)

测试策略

单元测试

javascript
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { Parser } from '../src/parser.js'

describe('Parser 测试', () => {
  it('应该解析模块', () => {
    const parser = new Parser()
    const source = 'export const x = 1'
    const result = parser.parse(source, 'test.js')
    assert.ok(result)
    assert.ok(result.ast)
  })
  
  it('应该提取依赖', () => {
    const parser = new Parser()
    const source = 'import { foo } from "./foo"'
    const result = parser.parse(source, 'test.js')
    assert.ok(result.dependencies.includes('./foo'))
  })
})

性能优化

1. 增量构建

javascript
function buildIncremental(changedFiles) {
  const affected = getAffectedModules(changedFiles)
  
  for (const module of affected) {
    rebuildModule(module)
  }
}

2. 并行处理

javascript
async function buildParallel(modules) {
  const chunks = chunkArray(modules, os.cpus().length)
  
  const promises = chunks.map(chunk => 
    Promise.all(chunk.map(m => buildModule(m)))
  )
  
  return Promise.all(promises)
}

3. 缓存

javascript
const cache = new Map()

function build(module) {
  if (cache.has(module.id)) {
    return cache.get(module.id)
  }
  
  const result = buildInternal(module)
  cache.set(module.id, result)
  return result
}

总结

Rollup 的源代码体现了以下设计原则:

  1. ES6 优先:专注于 ES6 模块
  2. Tree-shaking:移除未使用的代码
  3. 高效:使用 Magic String 和 estree-walker
  4. 插件化:支持插件扩展

理解源代码有助于更好地使用和优化 Rollup。

参考资源

架构师AI杜公众号二维码

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