Skip to content

Vite 课程计划

课程概述

本课程将带你从零开始实现一个简化版的 Vite,理解 Vite 的核心工作原理。

课程目标

  • 理解静态站点生成器的工作原理
  • 掌握模块解析和依赖管理
  • 学习热模块替换(HMR)的实现
  • 掌握测试驱动开发(TDD)
  • 能够构建自己的开发工具

课程结构

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


第 1 课:课程介绍与环境准备

目标

  • 了解课程内容和目标
  • 搭建开发环境
  • 创建项目结构
  • 运行第一个测试

内容

  1. 课程介绍

    • Vite 是什么
    • 为什么学习 Vite
    • 课程目标
  2. 环境准备

    • 安装 Node.js(>= 18)
    • 验证 Node.js 版本
    • 初始化项目
  3. 项目结构

    • 创建目录结构
    • 初始化 package.json
    • 配置测试框架

实践步骤

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

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

# 3. 创建目录结构
mkdir -p src test

# 4. 创建 package.json
cat > package.json << 'EOF'
{
  "name": "vite-course",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "node --test test/*.test.js"
  }
}
EOF

# 5. 创建第一个测试
cat > test/hello.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'

describe('Hello Test', () => {
  it('should pass', () => {
    assert.strictEqual(1 + 1, 2)
  })
})
EOF

# 6. 运行测试
npm test

预期输出

✓ should pass (1ms)

测试验证

bash
npm test

应该看到测试通过。

总结

  • ✅ 创建了项目结构
  • ✅ 配置了测试框架
  • ✅ 运行了第一个测试

第 2 课:模块图基础

目标

  • 理解模块图的概念
  • 实现基本的模块图
  • 编写模块图测试

内容

  1. 模块图概念

    • 什么是模块图
    • 模块图的作用
    • 模块图的数据结构
  2. 实现模块图

    • 创建模块节点
    • 添加导入关系
    • 追踪依赖
  3. 测试模块图

    • 创建测试用例
    • 验证功能

实践步骤

bash
# 1. 创建模块图文件
cat > src/module-graph.js << 'EOF'
// 模块图 - 追踪模块依赖关系
export function createModuleGraph() {
  const modules = new Map()
  const urlToModuleMap = new Map()

  return {
    // 创建模块
    createModule(url) {
      const id = generateId(url)
      const module = {
        id,
        url,
        imports: new Set(),
        importers: new Set()
      }

      modules.set(id, module)
      urlToModuleMap.set(url, module)

      return module
    },

    // 获取模块
    getModule(url) {
      return urlToModuleMap.get(url)
    },

    // 添加导入关系
    addImport(importerUrl, importedUrl) {
      const importer = this.getModule(importerUrl) || this.createModule(importerUrl)
      const imported = this.getModule(importedUrl) || this.createModule(importedUrl)

      importer.imports.add(imported)
      imported.importers.add(importer)
    }
  }
}

// 生成模块 ID
function generateId(url) {
  return url.replace(/\//g, '_').replace(/\./g, '_')
}
EOF

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

describe('模块图测试', () => {
  it('应该创建模块', () => {
    const graph = createModuleGraph()
    const module = graph.createModule('/src/main.js')

    assert.strictEqual(module.url, '/src/main.js')
    assert.strictEqual(module.imports.size, 0)
    assert.strictEqual(module.importers.size, 0)
  })

  it('应该获取模块', () => {
    const graph = createModuleGraph()
    graph.createModule('/src/main.js')

    const module = graph.getModule('/src/main.js')

    assert.ok(module)
  })

  it('应该添加导入关系', () => {
    const graph = createModuleGraph()
    graph.addImport('/src/main.js', '/src/utils.js')

    const main = graph.getModule('/src/main.js')
    const utils = graph.getModule('/src/utils.js')

    assert.ok(main.imports.has(utils))
    assert.ok(utils.importers.has(main))
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建模块
✓ 应该获取模块
✓ 应该添加导入关系

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了基本的模块图
  • ✅ 理解了模块图的概念
  • ✅ 编写了模块图测试

第 3 课:中间件系统

目标

  • 理解中间件的概念
  • 实现基本的中间件
  • 处理不同类型的请求

内容

  1. 中间件概念

    • 什么是中间件
    • 中间件的作用
    • 中间件的执行顺序
  2. 实现中间件

    • 创建 HTTP 服务器
    • 实现请求处理
    • 路由请求到处理器
  3. 测试中间件

    • 测试请求处理
    • 验证响应

实践步骤

bash
# 1. 创建中间件文件
cat > src/middleware.js << 'EOF'
import { createServer } from 'http'

// 创建中间件
export function createMiddleware(handlers) {
  return async function middleware(req, res) {
    const url = req.url

    for (const handler of handlers) {
      const result = await handler(url, req, res)
      if (result) {
        return
      }
    }

    res.statusCode = 404
    res.end('Not Found')
  }
}

// 创建开发服务器
export function createDevServer(options = {}) {
  const { port = 5173 } = options

  const handlers = [
    handleHTML,
    handleJS,
    handleStatic
  ]

  const middleware = createMiddleware(handlers)

  const server = createServer((req, res) => {
    middleware(req, res)
  })

  server.listen(port, () => {
    console.log(`服务器运行在 http://localhost:${port}`)
  })

  return server
}

// 处理 HTML
function handleHTML(url, req, res) {
  if (url === '/' || url.endsWith('.html')) {
    res.setHeader('Content-Type', 'text/html; charset=utf-8')
    res.end('<!DOCTYPE html><html><body>Hello Vite</body></html>')
    return true
  }
  return false
}

// 处理 JS
function handleJS(url, req, res) {
  if (url.endsWith('.js')) {
    res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
    res.end('console.log("Hello from Vite")')
    return true
  }
  return false
}

// 处理静态资源
function handleStatic(url, req, res) {
  res.statusCode = 404
  res.end('Not Found')
  return false
}
EOF

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

describe('中间件测试', () => {
  it('应该处理 HTML 请求', async () => {
    const handlers = [
      (url, req, res) => {
        if (url === '/') {
          res.end('HTML')
          return true
        }
        return false
      }
    ]

    const middleware = createMiddleware(handlers)
    const req = { url: '/' }
    const res = { end: (data) => { res.data = data }, setHeader: () => {} }

    await middleware(req, res)

    assert.strictEqual(res.data, 'HTML')
  })

  it('应该处理 JS 请求', async () => {
    const handlers = [
      (url, req, res) => {
        if (url.endsWith('.js')) {
          res.end('JS')
          return true
        }
        return false
      }
    ]

    const middleware = createMiddleware(handlers)
    const req = { url: '/main.js' }
    const res = { end: (data) => { res.data = data }, setHeader: () => {} }

    await middleware(req, res)

    assert.strictEqual(res.data, 'JS')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该处理 HTML 请求
✓ 应该处理 JS 请求

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了基本的中间件系统
  • ✅ 理解了中间件的概念
  • ✅ 创建了开发服务器

第 4 课:JavaScript 转换

目标

  • 理解代码转换的概念
  • 使用 esbuild 转换代码
  • 实现代码转换中间件

内容

  1. 代码转换概念

    • 为什么需要代码转换
    • esbuild 简介
    • 转换流程
  2. 实现代码转换

    • 安装 esbuild
    • 实现转换函数
    • 集成到中间件
  3. 测试代码转换

    • 测试 JS 转换
    • 测试 JSX 转换
    • 测试 TS 转换

实践步骤

bash
# 1. 安装 esbuild
npm install esbuild

# 2. 创建转换文件
cat > src/transform.js << 'EOF'
import { transform } from 'esbuild'

// 转换代码
export async function transformCode(code, options = {}) {
  const {
    loader = 'js',
    target = 'es2020',
    format = 'esm'
  } = options

  const result = await transform(code, {
    loader,
    target,
    format,
    sourcemap: true
  })

  return result
}

// 获取加载器
export function getLoader(filePath) {
  const ext = filePath.split('.').pop()
  const loaders = {
    'js': 'js',
    'jsx': 'jsx',
    'ts': 'ts',
    'tsx': 'tsx'
  }
  return loaders[ext] || 'js'
}
EOF

# 3. 创建测试文件
cat > test/transform.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { transformCode, getLoader } from '../src/transform.js'

describe('代码转换测试', () => {
  it('应该转换 JS 代码', async () => {
    const code = 'const a = 1'
    const result = await transformCode(code)

    assert.ok(result.code)
    assert.ok(result.code.includes('const'))
  })

  it('应该转换 JSX 代码', async () => {
    const code = 'const App = () => <div>Hello</div>'
    const result = await transformCode(code, { loader: 'jsx' })

    assert.ok(result.code)
    assert.ok(!result.code.includes('<div>'))
  })

  it('应该获取加载器', () => {
    assert.strictEqual(getLoader('main.js'), 'js')
    assert.strictEqual(getLoader('app.jsx'), 'jsx')
    assert.strictEqual(getLoader('utils.ts'), 'ts')
    assert.strictEqual(getLoader('component.tsx'), 'tsx')
  })
})
EOF

# 4. 运行测试
npm test

预期输出

✓ 应该转换 JS 代码
✓ 应该转换 JSX 代码
✓ 应该获取加载器

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 使用 esbuild 转换代码
  • ✅ 理解了代码转换的概念
  • ✅ 实现了代码转换功能

第 5 课:CSS 处理

目标

  • 理解 CSS 处理的概念
  • 实现 CSS 转换
  • 将 CSS 注入到页面

内容

  1. CSS 处理概念

    • 为什么需要 CSS 处理
    • CSS 转换方法
    • CSS 注入方式
  2. 实现 CSS 处理

    • 读取 CSS 文件
    • 转换为 JavaScript
    • 注入到页面
  3. 测试 CSS 处理

    • 测试 CSS 读取
    • 测试 CSS 转换

实践步骤

bash
# 1. 创建 CSS 处理文件
cat > src/css.js << 'EOF'
// 处理 CSS
export function transformCSS(css) {
  // 将 CSS 转换为 JavaScript
  const jsCode = `
    (function() {
      const style = document.createElement('style');
      style.textContent = ${JSON.stringify(css)};
      document.head.appendChild(style);
    })();
  `

  return jsCode
}

// 解析 CSS 导入
export function parseCSSImports(css) {
  const imports = []
  const regex = /@import\s+['"]([^'"]+)['"]/g

  let match
  while ((match = regex.exec(css)) !== null) {
    imports.push(match[1])
  }

  return imports
}
EOF

# 2. 创建测试文件
cat > test/css.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { transformCSS, parseCSSImports } from '../src/css.js'

describe('CSS 处理测试', () => {
  it('应该转换 CSS', () => {
    const css = 'body { color: red; }'
    const result = transformCSS(css)

    assert.ok(result.includes('style'))
    assert.ok(result.includes('color: red'))
  })

  it('应该解析 CSS 导入', () => {
    const css = '@import "style.css"; body { color: red; }'
    const imports = parseCSSImports(css)

    assert.strictEqual(imports.length, 1)
    assert.strictEqual(imports[0], 'style.css')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该转换 CSS
✓ 应该解析 CSS 导入

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了 CSS 处理
  • ✅ 理解了 CSS 转换的概念
  • ✅ 将 CSS 转换为 JavaScript

第 6 课:依赖优化

目标

  • 理解依赖优化的概念
  • 实现依赖扫描
  • 实现依赖预构建

内容

  1. 依赖优化概念

    • 为什么需要依赖优化
    • 预构建的好处
    • 缓存策略
  2. 实现依赖优化

    • 扫描依赖
    • 预构建依赖
    • 缓存管理
  3. 测试依赖优化

    • 测试依赖扫描
    • 测试预构建

实践步骤

bash
# 1. 创建依赖优化文件
cat > src/dep-optimizer.js << 'EOF'
import { readFile, writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'

// 依赖优化器
export function createDepOptimizer(root) {
  const cacheDir = join(root, 'node_modules/.vite')
  const metadataPath = join(cacheDir, '_metadata.json')
  const deps = new Map()

  return {
    // 扫描依赖
    async scanDeps() {
      const packageJsonPath = join(root, 'package.json')
      const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))

      const dependencies = {
        ...packageJson.dependencies,
        ...packageJson.devDependencies
      }

      return Object.keys(dependencies)
    },

    // 预构建依赖
    async preBundle() {
      const depList = await this.scanDeps()

      if (depList.length === 0) {
        return
      }

      console.log('预构建依赖:', depList)

      // 保存到缓存
      for (const dep of depList) {
        deps.set(`/node_modules/${dep}`, `// 预构建的 ${dep}`)
      }
    },

    // 获取依赖
    getDep(url) {
      return deps.get(url)
    }
  }
}
EOF

# 2. 创建测试文件
cat > test/dep-optimizer.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { createDepOptimizer } from '../src/dep-optimizer.js'

describe('依赖优化测试', () => {
  it('应该扫描依赖', async () => {
    const optimizer = createDepOptimizer(process.cwd())
    const deps = await optimizer.scanDeps()

    assert.ok(Array.isArray(deps))
    assert.ok(deps.length > 0)
  })

  it('应该预构建依赖', async () => {
    const optimizer = createDepOptimizer(process.cwd())
    await optimizer.preBundle()

    const dep = optimizer.getDep('/node_modules/esbuild')

    assert.ok(dep)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该扫描依赖
✓ 应该预构建依赖

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了依赖优化
  • ✅ 理解了依赖预构建的概念
  • ✅ 实现了缓存管理

第 7 课:集成所有功能

目标

  • 集成所有功能
  • 创建完整的开发服务器
  • 测试完整流程

内容

  1. 功能集成

    • 整合模块图
    • 整合中间件
    • 整合代码转换
    • 整合 CSS 处理
    • 整合依赖优化
  2. 创建完整服务器

    • 创建主服务器文件
    • 配置所有功能
    • 启动服务器
  3. 测试完整流程

    • 测试 HTML 请求
    • 测试 JS 请求
    • 测试 CSS 请求

实践步骤

bash
# 1. 创建主服务器文件
cat > src/server.js << 'EOF'
import { createServer } from 'http'
import { createModuleGraph } from './module-graph.js'
import { createMiddleware } from './middleware.js'
import { transformCode, getLoader } from './transform.js'
import { transformCSS } from './css.js'
import { createDepOptimizer } from './dep-optimizer.js'
import { readFile } from 'fs/promises'
import { join } from 'path'

// 创建开发服务器
export async function createDevServer(options = {}) {
  const {
    root = process.cwd(),
    port = 5173
  } = options

  // 创建模块图
  const moduleGraph = createModuleGraph()

  // 创建依赖优化器
  const depOptimizer = createDepOptimizer(root)
  await depOptimizer.preBundle()

  // 创建中间件
  const handlers = [
    async (url, req, res) => {
      // 处理 HTML
      if (url === '/' || url.endsWith('.html')) {
        const filePath = join(root, url === '/' ? 'index.html' : url)
        const content = await readFile(filePath, 'utf-8')

        res.setHeader('Content-Type', 'text/html; charset=utf-8')
        res.end(content)
        return true
      }
      return false
    },

    async (url, req, res) => {
      // 处理 JS
      if (url.endsWith('.js')) {
        const filePath = join(root, url)
        const content = await readFile(filePath, 'utf-8')

        const result = await transformCode(content, {
          loader: getLoader(filePath)
        })

        moduleGraph.updateModule(url, result.code)

        res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
        res.end(result.code)
        return true
      }
      return false
    },

    async (url, req, res) => {
      // 处理 CSS
      if (url.endsWith('.css')) {
        const filePath = join(root, url)
        const content = await readFile(filePath, 'utf-8')

        const jsCode = transformCSS(content)

        res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
        res.end(jsCode)
        return true
      }
      return false
    }
  ]

  const middleware = createMiddleware(handlers)

  const server = createServer((req, res) => {
    middleware(req, res)
  })

  server.listen(port, () => {
    console.log(`Vite 开发服务器运行在 http://localhost:${port}`)
  })

  return server
}

// 如果直接运行此文件,启动服务器
if (import.meta.url === `file://${process.argv[1]}`) {
  createDevServer()
}
EOF

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

describe('服务器测试', () => {
  it('应该创建服务器', async () => {
    const server = await createDevServer({ port: 5174 })

    assert.ok(server)

    // 关闭服务器
    server.close()
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建服务器

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 集成了所有功能
  • ✅ 创建了完整的开发服务器
  • ✅ 测试了完整流程

第 9 课:HMR 功能

目标

  • 理解 HMR 的概念
  • 实现 HMR 管理器
  • 测试 HMR 功能

内容

  1. HMR 概念

    • 什么是 HMR
    • HMR 的优势
    • HMR 的工作原理
  2. 实现 HMR 管理器

    • 文件监听
    • 查找受影响的模块
    • 发送 HMR 更新
    • 整页刷新
  3. 测试 HMR

    • 测试文件监听
    • 测试模块更新
    • 测试消息发送

实践步骤

bash
# 1. 创建 HMR 管理器文件
cat > src/hmr.js << 'EOF'
import { watch } from 'fs'
import { join } from 'path'

// HMR 管理器
export function createHMRManager(moduleGraph, ws) {
  const watchers = new Map()

  return {
    // 启动文件监听
    startWatch(root) {
      const watcher = watch(root, { recursive: true }, (eventType, filename) => {
        if (eventType === 'change' && filename) {
          this.handleFileChange(filename)
        }
      })

      watchers.set(root, watcher)
    },

    // 处理文件变化
    async handleFileChange(filename) {
      const url = \`/\${filename}\`

      // 1. 找到受影响的模块
      const affectedModules = this.findAffectedModules(url)

      if (affectedModules.length === 0) {
        return
      }

      // 2. 检查模块是否接受 HMR
      const acceptedModules = this.filterAcceptedModules(affectedModules)

      if (acceptedModules.length === 0) {
        // 整页刷新
        this.sendFullReload()
        return
      }

      // 3. 更新模块
      for (const module of acceptedModules) {
        await this.updateModule(module)
      }

      // 4. 发送 HMR 更新
      this.sendHMRUpdate(acceptedModules)
    },

    // 找到受影响的模块
    findAffectedModules(url) {
      const modules = []
      const visited = new Set()
      const queue = [url]

      while (queue.length > 0) {
        const current = queue.shift()
        if (visited.has(current)) continue

        visited.add(current)

        const module = moduleGraph.getModule(current)
        if (module) {
          modules.push(module)

          // 添加导入者
          for (const importer of module.importers) {
            queue.push(importer.url)
          }
        }
      }

      return modules
    },

    // 过滤接受 HMR 的模块
    filterAcceptedModules(modules) {
      return modules.filter(module => module.acceptsHMR)
    },

    // 更新模块
    async updateModule(module) {
      // 重新转换模块
      const newCode = await this.transformModule(module.url)
      moduleGraph.updateModule(module.url, newCode)
      module.lastHMRTimestamp = Date.now()
    },

    // 转换模块
    async transformModule(url) {
      // 这里应该调用实际的转换逻辑
      return ''
    },

    // 发送 HMR 更新
    sendHMRUpdate(modules) {
      const updates = modules.map(module => ({
        type: 'js-update',
        timestamp: Date.now(),
        path: module.url,
        acceptedPath: module.url
      }))

      ws.send({
        type: 'update',
        updates
      })
    },

    // 发送整页刷新
    sendFullReload() {
      ws.send({
        type: 'full-reload'
      })
    }
  }
}
EOF

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

describe('HMR 管理器测试', () => {
  it('应该创建 HMR 管理器', () => {
    const moduleGraph = { getModule: () => null }
    const ws = { send: () => {} }

    const hmr = createHMRManager(moduleGraph, ws)

    assert.ok(hmr)
  })

  it('应该找到受影响的模块', () => {
    const moduleA = { url: '/src/a.js', importers: new Set(), imports: new Set() }
    const moduleB = { url: '/src/b.js', importers: new Set([moduleA]), imports: new Set() }

    const moduleGraph = {
      getModule: (url) => {
        if (url === '/src/a.js') return moduleA
        if (url === '/src/b.js') return moduleB
        return null
      }
    }

    const ws = { send: () => {} }
    const hmr = createHMRManager(moduleGraph, ws)

    const affected = hmr.findAffectedModules('/src/b.js')

    assert.ok(affected.includes(moduleB))
    assert.ok(affected.includes(moduleA))
  })

  it('应该发送 HMR 更新', () => {
    const moduleA = { url: '/src/a.js', lastHMRTimestamp: 0 }
    const moduleB = { url: '/src/b.js', lastHMRTimestamp: 0 }

    let sentData = null
    const ws = {
      send: (data) => {
        sentData = data
      }
    }

    const moduleGraph = { getModule: () => null }
    const hmr = createHMRManager(moduleGraph, ws)

    hmr.sendHMRUpdate([moduleA, moduleB])

    assert.strictEqual(sentData.type, 'update')
    assert.strictEqual(sentData.updates.length, 2)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建 HMR 管理器
✓ 应该找到受影响的模块
✓ 应该发送 HMR 更新

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了 HMR 管理器
  • ✅ 理解了 HMR 的工作原理
  • ✅ 实现了文件监听和模块更新

第 10 课:插件系统

目标

  • 理解插件系统的概念
  • 实现插件容器
  • 创建示例插件

内容

  1. 插件系统概念

    • 什么是插件系统
    • 插件的作用
    • 钩子系统
  2. 实现插件容器

    • 插件接口
    • 钩子执行
    • 插件管理
  3. 创建示例插件

    • 转换插件
    • 虚拟模块插件

实践步骤

bash
# 1. 创建插件容器文件
cat > src/plugin-container.js << 'EOF'
// 插件容器
export function createPluginContainer(plugins = []) {
  const hooks = new Map()

  // 收集插件钩子
  for (const plugin of plugins) {
    if (!plugin.name) {
      throw new Error('Plugin must have a name')
    }

    // 配置钩子
    if (plugin.config) {
      addHook('config', plugin.config)
    }

    // 转换钩子
    if (plugin.transform) {
      addHook('transform', plugin.transform)
    }
  }

  function addHook(name, hook) {
    if (!hooks.has(name)) {
      hooks.set(name, [])
    }
    hooks.get(name).push(hook)
  }

  return {
    // 执行钩子
    async runHook(name, ...args) {
      const hookList = hooks.get(name)
      if (!hookList) return

      for (const hook of hookList) {
        const result = await hook(...args)
        if (result !== undefined) {
          return result
        }
      }
    },

    // 转换钩子
    async transform(code, id) {
      return await this.runHook('transform', code, id)
    }
  }
}

// 插件类
export class Plugin {
  constructor(options = {}) {
    this.name = options.name || 'anonymous-plugin'
    this.config = options.config
    this.transform = options.transform
  }
}
EOF

# 2. 创建测试文件
cat > test/plugin-container.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { createPluginContainer, Plugin } from '../src/plugin-container.js'

describe('插件容器测试', () => {
  it('应该创建插件容器', () => {
    const container = createPluginContainer([])
    assert.ok(container)
  })

  it('应该执行转换钩子', async () => {
    const plugin = new Plugin({
      name: 'test-plugin',
      transform(code, id) {
        if (id.endsWith('.js')) {
          return code.replace('foo', 'bar')
        }
      }
    })

    const container = createPluginContainer([plugin])
    const result = await container.transform('const foo = 1', '/test.js')

    assert.strictEqual(result, 'const bar = 1')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建插件容器
✓ 应该执行转换钩子

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了插件系统
  • ✅ 理解了插件的概念
  • ✅ 创建了插件容器

第 11 课:WebSocket 通信

目标

  • 理解 WebSocket 的概念
  • 实现 WebSocket 服务器
  • 实现 HMR 消息

内容

  1. WebSocket 概念

    • 什么是 WebSocket
    • WebSocket 的优势
    • WebSocket 在 HMR 中的应用
  2. 实现 WebSocket 服务器

    • 创建服务器
    • 管理连接
    • 发送消息
  3. 实现 HMR 消息

    • HMR 更新消息
    • 整页刷新消息
    • 错误消息

实践步骤

bash
# 1. 创建 WebSocket 服务器文件
cat > src/websocket.js << 'EOF'
import { createServer as createHTTPServer } from 'http'
import { WebSocketServer } from 'ws'

// WebSocket 服务器
export function createWebSocketServer(options = {}) {
  const { port = 24678, host = 'localhost' } = options

  const httpServer = createHTTPServer()
  const wss = new WebSocketServer({ server: httpServer })
  const clients = new Set()

  wss.on('connection', (ws) => {
    console.log('WebSocket 客户端已连接')
    clients.add(ws)

    ws.on('message', (data) => {
      handleMessage(ws, data)
    })

    ws.on('close', () => {
      console.log('WebSocket 客户端已断开')
      clients.delete(ws)
    })
  })

  httpServer.listen(port, host, () => {
    console.log(\`WebSocket 服务器运行在 ws://\${host}:\${port}\`)
  })

  return {
    broadcast(data) {
      const message = JSON.stringify(data)
      for (const client of clients) {
        if (client.readyState === 1) {
          client.send(message)
        }
      }
    }
  }
}

// 处理消息
function handleMessage(ws, data) {
  try {
    const message = JSON.parse(data.toString())
    console.log('收到消息:', message)
  } catch (error) {
    console.error('解析消息失败:', error)
  }
}
EOF

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

describe('WebSocket 服务器测试', () => {
  it('应该创建 WebSocket 服务器', () => {
    const wsServer = createWebSocketServer({ port: 24679 })
    assert.ok(wsServer)
    wsServer.close()
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建 WebSocket 服务器

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了 WebSocket 服务器
  • ✅ 理解了 WebSocket 的概念
  • ✅ 实现了消息广播

第 12 课:代理功能

目标

  • 理解代理的概念
  • 实现代理中间件
  • 实现路径重写

内容

  1. 代理概念

    • 什么是代理
    • 为什么需要代理
    • 代理的应用场景
  2. 实现代理中间件

    • HTTP 代理
    • 路径重写
    • 多代理配置
  3. 测试代理功能

    • 测试请求转发
    • 测试路径重写

实践步骤

bash
# 1. 创建代理中间件文件
cat > src/proxy.js << 'EOF'
import { request as httpRequest } from 'http'
import { URL } from 'url'

// 代理中间件
export function createProxyMiddleware(options = {}) {
  const { target, changeOrigin = true, rewrite = (path) => path } = options

  if (!target) {
    throw new Error('Proxy target is required')
  }

  const targetUrl = new URL(target)

  return async function proxyMiddleware(req, res) {
    const url = req.url
    const path = rewrite(url)

    const proxyOptions = {
      hostname: targetUrl.hostname,
      port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
      path,
      method: req.method,
      headers: {
        ...req.headers,
        host: changeOrigin ? targetUrl.host : req.headers.host
      }
    }

    const proxyReq = httpRequest(proxyOptions, (proxyRes) => {
      Object.keys(proxyRes.headers).forEach(key => {
        res.setHeader(key, proxyRes.headers[key])
      })

      res.statusCode = proxyRes.statusCode
      proxyRes.pipe(res)
    })

    proxyReq.on('error', (error) => {
      console.error('代理错误:', error)
      res.statusCode = 502
      res.end('Bad Gateway')
    })

    req.pipe(proxyReq)
  }
}

// 路径重写工具
export const pathRewrite = {
  removePrefix(prefix) {
    return (path) => path.replace(new RegExp(\`^\${prefix}\`), '')
  },

  addPrefix(prefix) {
    return (path) => \`\${prefix}\${path}\`
  }
}
EOF

# 2. 创建测试文件
cat > test/proxy.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { createProxyMiddleware, pathRewrite } from '../src/proxy.js'

describe('代理中间件测试', () => {
  it('应该创建代理中间件', () => {
    const middleware = createProxyMiddleware({
      target: 'http://localhost:3000'
    })

    assert.ok(middleware)
  })

  it('应该重写路径', () => {
    const rewrite = pathRewrite.removePrefix('/api')
    assert.strictEqual(rewrite('/api/users'), '/users')

    const addPrefix = pathRewrite.addPrefix('/api')
    assert.strictEqual(addPrefix('/users'), '/api/users')
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建代理中间件
✓ 应该重写路径

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了代理中间件
  • ✅ 理解了代理的概念
  • ✅ 实现了路径重写

第 13 课:生产构建

目标

  • 理解生产构建的概念
  • 实现构建器
  • 实现代码优化

内容

  1. 生产构建概念

    • 什么是生产构建
    • 为什么需要生产构建
    • 构建优化策略
  2. 实现构建器

    • HTML 构建
    • JavaScript 构建
    • 资源构建
  3. 实现代码优化

    • 代码压缩
    • 代码分割
    • Tree-shaking

实践步骤

bash
# 1. 创建构建器文件
cat > src/builder.js << 'EOF'
import { build } from 'esbuild'
import { minify } from 'terser'

// 生产构建器
export function createBuilder(options = {}) {
  const { root = process.cwd(), outDir = 'dist', minify = true } = options

  return {
    // 构建项目
    async build() {
      console.log('开始构建...')

      // 1. 构建 JavaScript
      await this.buildJS()

      // 2. 压缩代码
      if (minify) {
        await this.minifyOutput()
      }

      console.log('构建完成!')
    },

    // 构建 JavaScript
    async buildJS() {
      const result = await build({
        entryPoints: ['index.js'],
        bundle: true,
        format: 'esm',
        outdir: outDir,
        write: true
      })

      console.log('构建 JavaScript 完成')
    },

    // 压缩输出
    async minifyOutput() {
      console.log('压缩代码...')
      // 这里应该实现实际的压缩逻辑
      console.log('压缩完成')
    }
  }
}
EOF

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

describe('生产构建器测试', () => {
  it('应该创建构建器', () => {
    const builder = createBuilder({
      root: process.cwd(),
      outDir: 'dist'
    })

    assert.ok(builder)
  })
})
EOF

# 3. 运行测试
npm test

预期输出

✓ 应该创建构建器

测试验证

bash
npm test

所有测试应该通过。

总结

  • ✅ 实现了生产构建器
  • ✅ 理解了生产构建的概念
  • ✅ 实现了代码优化

第 14 课:总结与扩展

目标

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

内容

  1. 课程总结

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

    • 添加更多插件
    • 优化性能
    • 支持更多框架
  3. 下一步

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

总结

通过本课程,你学会了:

  1. ✅ 模块图的概念和实现
  2. ✅ 中间件系统的设计和实现
  3. ✅ 代码转换(esbuild)
  4. ✅ CSS 处理
  5. ✅ 依赖优化
  6. ✅ HMR 功能
  7. ✅ 插件系统
  8. ✅ WebSocket 通信
  9. ✅ 代理功能
  10. ✅ 生产构建

下一步

  • 学习 Vite 源码
  • 添加更多插件
  • 参与开源项目
  • 构建自己的工具

参考资源

架构师AI杜公众号二维码

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