Build Tool Deep Dive

结合源码
详解 Vite 原理

深入 Vite 源码仓库,理解其原生 ESM 驱动的开发服务器毫秒级 HMR Rollup 生产构建是如何协同工作, 彻底重塑前端开发体验的。

源码版本 5.x阅读时长 ~25 min深度原理

Vite 核心优势

Why is it so fast?

冷启动时间~300ms
HMR 热更新<50ms
打包即可开发0 次
生产级构建Rollup
The Problem

传统打包器的困境

Webpack 等工具将所有模块打包为 Bundle 再交给浏览器,项目越大,启动越慢。

传统打包器

Bundle-Based

解析入口递归打包生成Bundle启动服务
webpack 启动流程伪代码
// Webpack 启动时必须完成全量打包
entry → resolve → transform → chunk → bundle
// 1000 个模块 ≈ 3~30s 冷启动
// 改一个文件 → 重新打包受影响 chunk ≈ 1~5s
Vite

ESM-Native

启动服务按需编译浏览器原生加载
vite 启动流程伪代码
// Vite 不打包!直接启动 Dev Server
server = createServer(config)
await server.listen()
// 浏览器请求时才编译单个模块
// 1000 个模块 ≈ 300ms 冷启动
// 改一个文件 → 只重编译该模块 ≈ 20ms
Architecture

Vite 源码架构全景

Vite 的源码仓库包含多个核心模块,每个模块各司其职,构成完整的工具链。

Dev Server

基于 connect 的轻量 HTTP 服务器。接收浏览器请求,通过 Plugin Pipeline 编译模块并返回 ESM 响应。源码:packages/vite/src/node/server/

HMR 引擎

基于 WebSocket 的热更新通道。精确计算更新边界,仅替换变更模块。源码:packages/vite/src/node/hmr/

Plugin 系统

兼容 Rollup 插件接口,扩展了开发阶段独有钩子(configureServer, transformIndexHtml 等)。源码:packages/vite/src/node/plugin/

Build (Rollup)

生产构建直接委托给 Rollup,Vite 在其上叠加 CSS 处理、HTML 处理等插件。源码:packages/vite/src/node/build.ts

依赖预构建

使用 esbuild 将 CommonJS/UMD 依赖转为 ESM 并打包,缓存于 node_modules/.vite/。源码:packages/vite/src/node/optimizer/

CSS / 静态资源

内置 CSS Modules、PostCSS、静态资源处理,所有资源均通过 Plugin Pipeline。源码:packages/vite/src/node/plugins/

Source Code 01

创建开发服务器

当你在终端执行 vite 时, 入口文件是 packages/vite/src/node/cli.ts, 它最终调用 createServer() 创建实例:

packages/vite/src/node/server/index.tsTypeScript
export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  // 1. 解析配置:合并默认配置 + 用户 vite.config.ts
  const config = await resolveConfig(inlineConfig, 'serve')

  // 2. 创建中间件容器(基于 connect)
  const middlewares = connect()

  // 3. 创建 WebSocket 服务器(用于 HMR 通信)
  const ws = createWebSocketServer(httpServer, config)

  // 4. 初始化插件容器
  const pluginContainer = await createPluginContainer(config)

  // 5. 注册内置中间件(静态文件、HTML回退、转换等)
  // ── 被封装在 internalPlugins 中
  for (const plugin of config.plugins) {
    if (plugin.configureServer) {
      await plugin.configureServer(server)
    }
  }

  // 6. 返回完整的 server 实例
  const server: ViteDevServer = {
    config,
    middlewares,
    ws,
    pluginContainer,
    transformRequest(url) { /* ... */ },
    listen(port) { /* ... */ },
    // ...
  }
  return server
}

关键设计点

  • 为什么用 connect 而不是 Express?

    connect 极轻量(无路由层),Vite 只需中间件管道,不需 Express 的额外抽象。

  • pluginContainer 是什么?

    它是 Rollup 插件钩子的运行容器,负责 resolveId → load → transform 的管线调度。

  • configureServer 钩子的作用?

    允许插件在服务启动前注入自定义中间件或拦截请求,如 vite-plugin-mock。

Source Code 02

依赖预构建 (Pre-Bundling)

?为什么需要预构建?

npm 上的大多数包仍然以 CommonJSUMD 格式发布。 浏览器的原生 ESM 无法直接 import 这些模块。

此外,像 lodash-es 这样的包包含 600+ 个内部模块文件。 如果逐个请求,浏览器会发起大量 HTTP 请求,导致严重的瀑布流延迟。

预构建的核心目标:将 CJS→ESM 转换 + 内部模块合并为单文件, 结果缓存到 node_modules/.vite/deps/

packages/vite/src/node/optimizer/index.tsTypeScript
async function runOptimizeDeps(config) {
  // 1. 扫描入口文件,收集裸模块引用
  const deps = await scanImports(config)
  // deps = { 'react': 'react', 
  //          'react-dom': 'react-dom', ... }

  // 2. 使用 esbuild 进行打包
  const result = await esbuild.build({
    entryPoints: Object.keys(deps),
    bundle: true,          // 合并内部模块
    format: 'esm',         // 输出 ESM 格式
    outdir: depsCacheDir,  // .vite/deps/
    splitting: true,       // 代码分割
    plugins: [
      cjsToEsmPlugin()     // CJS → ESM 转换
    ],
  })

  // 3. 写入元数据(哈希 + 依赖映射)
  writeDepsMetadata(depsCacheDir, {
    hash: getDepHash(deps, config),
    optimized: deps,
  })
}

预构建流程图解

扫描入口scanImports
收集依赖裸模块解析
esbuild 打包CJS→ESM + Bundle
缓存输出.vite/deps/
Source Code 03

模块请求处理管线

当浏览器遇到 import App from './App.tsx' 时, 会向 Vite Dev Server 发起 HTTP 请求。Vite 通过 中间件 + 插件管线 处理该请求。

1

transformMiddleware 拦截请求

connect 中间件捕获对 .ts/.tsx/.vue/.css 等文件的请求,进入 transform 管线。

2

resolveId — 模块路径解析

pluginContainer.resolveId() 遍历所有插件的 resolveId 钩子,将 import 路径转换为文件系统绝对路径。

3

load — 加载文件内容

pluginContainer.load() 读取文件内容。某些插件(如虚拟模块)可以在此阶段完全替换内容。

4

transform — 代码转换

pluginContainer.transform() 执行核心转换链:TypeScript → JavaScript、JSX → 函数调用、CSS 预处理等。

5

返回 ESM 响应

转换后的代码通过 HTTP 响应返回给浏览器。浏览器原生执行 ESM,无需额外编译。

packages/vite/src/node/server/transformRequest.tsTypeScript
async function transformRequest(
  url: string,
  server: ViteDevServer,
  options?: TransformResult
): Promise<TransformResult | null> {
  // 1. resolve:将 URL 转换为文件路径
  const id = (await server.pluginContainer.resolveId(url))?.id
    ?? url

  // 2. load:读取文件内容
  const loadResult = await server.pluginContainer.load(id)
  const code = loadResult?.code ?? await fs.readFile(id, 'utf-8')

  // 3. transform:应用所有插件的 transform 钩子
  const result = await server.pluginContainer.transform(code, id)

  // 4. 缓存结果(后续相同请求直接返回)
  moduleGraph.ensureEntryFromUrl(url)
  return {
    code: result?.code,
    map: result?.map,
    etag: getEtag(result?.code),
  }
}
Source Code 04

HMR 热模块替换原理

HMR 三步走

① 文件监听 → 检测变更

Vite 使用 chokidar 监听文件系统。当文件发生变化时,获取与变更模块关联的 HMR 边界。

watcher.on('change', async (file) => {
  const mods = moduleGraph.getModulesByFile(file)
  hmr(mods, file)
})

② 推送更新消息

通过 WebSocket 向客户端推送类型为 update 的消息,包含变更模块的路径和时间戳。

ws.send({
  type: 'update',
  updates: [{
    type: 'js-update',
    path: '/src/App.tsx',
    acceptedPath: '/src/App.tsx',
    timestamp: Date.now()
  }]
})

③ 客户端接受更新

浏览器端 Vite HMR Runtime 收到消息后,重新 import 更新后的模块,并调用 accept 回调。

// 浏览器端 HMR Runtime
const newMod = await import(
  modPath + '?t=' + timestamp
)
if (mod.callbacks) {
  mod.callbacks.forEach(cb => cb(newMod))
}

Module Graph(模块图)

Vite 维护一个内存中的模块依赖图。每个模块记录其 importers(谁引入了它)和 imported(它引入了谁), HMR 更新时据此精确计算影响边界。

packages/vite/src/node/server/moduleGraph.tsTypeScript
class ModuleNode {
  url: string
  file: string | null
  importers = new Set<ModuleNode>()
  importedModules = new Set<ModuleNode>()
  acceptedHmrDeps = new Set<ModuleNode>()
  transformResult: TransformResult | null
  lastHMRTimestamp = 0
}

class ModuleGraph {
  urlToModuleMap = new Map<string, ModuleNode>()
  fileToModulesMap = new Map<string, Set<ModuleNode>>()

  async ensureEntryFromUrl(rawUrl: string) {
    // URL → ModuleNode 的双向映射
    // 同时建立 import 关系图
  }

  invalidateModule(mod: ModuleNode) {
    mod.transformResult = null
    mod.lastHMRTimestamp = Date.now()
    // 递归标记所有 importers 为 stale
  }
}

HMR 更新边界示意

main.ts
App.tsx
Header.tsxCHANGED
utils.ts
Source Code 05

插件系统的设计哲学

1

兼容 Rollup 插件

Vite 插件直接实现了 Rollup 的 Plugin 接口,80% 的 Rollup 插件可零修改在 Vite 中使用。

2

扩展独有钩子

为了开发服务器的特殊需求,Vite 扩展了 configureServertransformIndexHtmlhandleHotUpdate 等钩子。

3

Plugin Ordering

通过 enforce: 'pre' | 'post' 控制插件执行顺序, 确保关键转换(如 JSX → JS)在正确时机发生。

packages/vite/src/node/server/pluginContainer.tsTypeScript
export async function createPluginContainer(config: ResolvedConfig) {
  // 按 enforce 排序: pre → normal → post
  const sortedPlugins = sortPlugins(config.plugins)

  const container = {
    async resolveId(rawId: string, importer?: string) {
      let id = rawId
      for (const plugin of sortedPlugins) {
        if (!plugin.resolveId) continue
        const result = await plugin.resolveId.call(
          ctx, id, importer, { ssr: false }
        )
        if (result) {
          id = typeof result === 'string' ? result : result.id
        }
      }
      return { id }
    },

    async load(id: string) {
      for (const plugin of sortedPlugins) {
        if (!plugin.load) continue
        const result = await plugin.load.call(ctx, id)
        if (result !== null) return result
      }
      return null
    },

    async transform(code: string, id: string) {
      for (const plugin of sortedPlugins) {
        if (!plugin.transform) continue
        const result = await plugin.transform.call(ctx, code, id)
        if (result !== null) {
          code = typeof result === 'string' ? result : result.code
        }
      }
      return { code }
    }
  }

  return container
}
Source Code 06

生产构建:Vite + Rollup

在生产环境中,Vite 不再使用 esbuild 进行打包,而是直接调用 Rollup 作为构建工具。 这是因为 Rollup 提供了更成熟的代码分割、Tree-shaking 和 Chunk 优化能力。

CSS 代码分割

自动提取异步 chunk 的 CSS,避免加载闪烁。

异步 Chunk 加载优化

自动预加载关联 chunk,减少瀑布流。

CSS Codegen

内联关键 CSS,异步加载非关键样式。

多页面应用支持

自动检测 HTML 入口并生成对应 bundle。

packages/vite/src/node/build.tsTypeScript
import { rollup } from 'rollup'

export async function build(inlineConfig) {
  const config = await resolveConfig(inlineConfig, 'build')

  // Vite 内置插件自动注入
  const vitePlugins = [
    viteResolvePlugin(config),    // 路径解析
    viteCssPlugin(config),        // CSS 处理
    viteAssetPlugin(config),      // 静态资源
    viteHtmlPlugin(config),       // HTML 处理
    viteDefinePlugin(config),     // 环境变量
    ...(config.plugins),          // 用户插件
  ]

  // 调用 Rollup 构建
  const bundle = await rollup({
    input: config.build.rollupOptions.input,
    plugins: vitePlugins,
    ...config.build.rollupOptions,
  })

  // 写入产物
  await bundle.write({
    dir: config.build.outDir,
    format: 'es',           // ESM 输出
    sourcemap: true,
    manualChunks: config.build.rollupOptions
      .manualChunks,
  })
}
Full Lifecycle

Vite 完整生命周期

Dev 模式生命周期

1解析 vite.config.ts + 合并 CLI 参数
2resolveConfig — 解析别名、环境变量、插件排序
3createServer — 创建 connect + WebSocket 实例
4预构建依赖 — esbuild 将 CJS → ESM + 打包
5启动 HTTP 服务 — 监听端口,注册中间件
6等待浏览器请求 — 按需 transform 单个模块
7文件变更 — chokidar 监听 → WebSocket 推送 HMR

Build 模式生命周期

1解析配置 + 应用 build 专属插件
2调用 Rollup.rollup() — 全量打包
3resolveId → load → transform 全量执行
4Tree-shaking + 代码分割 + Chunk 优化
5bundle.write() — 输出到 dist/
6生成 manifest.json(SSR / 路由映射)
7可选:SSR Bundle / Library 模式构建

核心总结

利用浏览器原生 ESM,开发阶段无需打包,冷启动极快
esbuild 驱动依赖预构建,解决 CJS 兼容和请求瀑布流
基于 WebSocket 的 HMR,精确模块级热更新
插件系统兼容 Rollup 并扩展开发专属钩子
生产构建委托 Rollup,获得成熟的优化能力
Module Graph 内存模型驱动 HMR 边界计算