Vite内核解析-第13章 Rolldown 构建引擎

《Vite 设计与实现》完整目录

第13章 Rolldown 构建引擎

如果说开发服务器是 Vite 的左手------灵活、快速、按需响应------那么构建引擎就是它的右手------全面、深入、系统优化。开发时按需编译的策略带来了极致的启动速度和即时反馈,但生产环境需要的是另一种品质:经过 tree-shaking 的精简代码、经过分割的并行加载包、经过压缩的最小体积、经过 hash 的长期缓存。Vite 的构建引擎承担着将开发时的源代码转化为生产级产物的使命。

在 Vite 的演进历程中,构建引擎经历了一次意义深远的技术迁移:底层打包器从 Rollup 切换到了 Rolldown。Rollup 是一个成熟的 JavaScript 打包器,以出色的 tree-shaking 和模块化输出著称;Rolldown 则是其 Rust 实现,保持了与 Rollup 几乎完全兼容的插件 API,同时在打包速度上实现了数量级的提升。这次迁移不仅是性能的飞跃,更标志着 Vite 技术栈向 Rust 生态的深度融合。

本章将深入 build.ts 这个近 1930 行的核心文件,从 build() 函数的入口出发,追踪配置解析、环境创建、Rolldown 选项构建、打包执行到产物输出的完整流程。

:::tip 本章要点

  • 理解 build() 函数的完整执行流程与架构设计
  • 掌握 Rolldown 与 Rollup 的兼容性设计和关键差异
  • 深入 resolveRolldownOptions 的配置生成逻辑
  • 理解 BuildEnvironment 与多环境构建的架构
  • 掌握 createBuilderbuildApp 的编排机制
  • 了解构建 Hook 注入、环境隔离与产物输出策略 :::

13.1 从 Rollup 到 Rolldown

为什么选择 Rolldown

Vite 最初选择 Rollup 作为构建引擎,这是一个经过十年打磨的 JavaScript 打包器。Rollup 的 tree-shaking 算法基于 ES Module 的静态结构分析,能精确地移除未使用的导出,生成高度优化的产物。然而,随着前端项目规模的爆炸式增长------现代应用动辄数千个模块、数十万行代码------Rollup 的 JavaScript 实现在处理大型项目时的性能瓶颈日益明显。模块解析、AST 遍历、代码生成等 CPU 密集型操作在 JavaScript 的单线程环境中难以并行化。

Rolldown 是 Rollup 的 Rust 重写,由 Vite 团队主导开发。Rust 的零开销抽象、精确的内存管理和天然的并行能力使得 Rolldown 在保持 API 兼容性的同时,打包速度提升了 10 倍乃至更多。更重要的是,Rolldown 与 Vite 的其他 Rust 组件(如 oxc 解析器和压缩器)共享底层基础设施,形成了一个高效的工具链闭环。

在 Vite 的源码中,我们可以清晰地看到这一迁移的痕迹:

typescript 复制代码
// build.ts 中的 import 声明
import type {
  RolldownBuild,
  RolldownOptions,
  RolldownOutput,
  RolldownWatcher,
  RollupError,    // 注意:错误类型仍使用 Rollup 的命名
} from 'rolldown'
import { viteLoadFallbackPlugin as nativeLoadFallbackPlugin } from 'rolldown/experimental'
import { esmExternalRequirePlugin } from 'rolldown/plugins'

类型名称从 Rollup* 变为 Rolldown*,但 RollupError 等兼容性类型保持不变,体现了渐进式迁移的策略。

兼容性保障

为了确保现有项目的平滑升级,Vite 在配置层面维护了完整的向后兼容性。rollupOptions 被保留为 rolldownOptions 的别名,使用旧配置名的项目不需要做任何修改就能正常工作:

typescript 复制代码
export interface BuildEnvironmentOptions {
  /**
   * Alias to `rolldownOptions`
   * @deprecated Use `rolldownOptions` instead.
   */
  rollupOptions?: RolldownOptions
  /**
   * Will be merged with internal rolldown options.
   */
  rolldownOptions?: RolldownOptions
}

在配置解析阶段,setupRollupOptionCompat 函数负责处理这种别名映射。这种设计让生态系统有充足的时间完成迁移,而不是一刀切地破坏兼容性。

13.2 构建流程全景

build() 入口函数

与人们对构建入口的预期相比,build() 函数出人意料地简洁。它的全部工作就是创建一个构建器实例,然后委托构建器对第一个环境执行构建。这种简洁性不是偷工减料,而是优秀的抽象设计------真正的复杂性被封装在了 createBuilderbuildEnvironment 中:

typescript 复制代码
export async function build(
  inlineConfig: InlineConfig = {},
): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
  const builder = await createBuilder(inlineConfig, true)
  const environment = Object.values(builder.environments)[0]
  if (!environment) throw new Error('No environment found')
  return builder.build(environment)
}

这个设计的深意在于:单环境构建和多环境构建共享完全相同的底层机制。build() 只是 createBuilder().build() 的快捷方式,当需要构建多个环境时,调用方可以直接使用 createBuilder 获得更精细的控制。

graph TD A["vite build 命令"] --> B["build(inlineConfig)"] B --> C["createBuilder(inlineConfig, true)"] C --> C1["resolveConfigToBuild()"] C1 --> C2["resolveConfig(inlineConfig, 'build')"] C2 --> C3["创建 ViteBuilder 实例"] C3 --> D["setupEnvironment()"] D --> D1["config.build.createEnvironment(name, config)"] D1 --> D2["new BuildEnvironment(name, config)"] D2 --> D3["environment.init()"] C3 --> E["builder.build(environment)"] E --> F["buildEnvironment(environment)"] F --> G["resolveRolldownOptions()"] G --> H{"watch 模式?"} H -->|是| I["rolldown watch() 启动文件监听"] I --> I1["返回 RolldownWatcher"] H -->|否| J["rolldown(options) 执行打包"] J --> K["bundle.write 或 bundle.generate"] K --> L["注入 chunk 元数据"] L --> M["返回 RolldownOutput"] style A fill:#e1f5fe style M fill:#e8f5e9 style I1 fill:#e8f5e9

resolveConfigToBuild:配置解析

构建配置的解析通过 resolveConfigToBuild 完成,它是通用的 resolveConfig 函数的构建模式封装。两个可选的回调参数------patchConfigpatchPlugins------是多环境构建的关键支撑,它们允许在配置解析完成后对结果进行环境特定的修补:

typescript 复制代码
function resolveConfigToBuild(
  inlineConfig: InlineConfig = {},
  patchConfig?: (config: ResolvedConfig) => void,
  patchPlugins?: (resolvedPlugins: Plugin[]) => void,
): Promise<ResolvedConfig> {
  return resolveConfig(
    inlineConfig,
    'build',          // command: 告诉插件系统当前是构建模式
    'production',     // mode: 默认的构建模式
    'production',     // configEnv.mode
    false,            // isPreview: 不是预览模式
    patchConfig,      // 配置后处理回调
    patchPlugins,     // 插件后处理回调
  )
}

13.3 构建选项解析

resolveBuildEnvironmentOptions:从用户配置到内部表示

这个函数承担着将用户友好的配置选项转换为内部精确表示的重任。它处理了废弃选项的兼容、默认值的合并、特殊值的展开等一系列转换工作。理解这个函数是理解 Vite 构建行为的基础:

typescript 复制代码
export function resolveBuildEnvironmentOptions(
  raw: BuildEnvironmentOptions,
  logger: Logger,
  consumer: 'client' | 'server' | undefined,
  isBundledDev: boolean,
): ResolvedBuildEnvironmentOptions {
  // 处理废弃的 polyfillModulePreload 选项
  const deprecatedPolyfillModulePreload = raw.polyfillModulePreload
  if (deprecatedPolyfillModulePreload !== undefined) {
    logger.warn('polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.')
  }

  // 合并默认值------注意 consumer 参数如何影响默认值
  const merged = mergeWithDefaults({
    ..._buildEnvironmentOptionsDefaults,
    cssCodeSplit: !raw.lib,
    minify: consumer === 'server' || isBundledDev ? false : 'oxc',
    ssr: consumer === 'server',
    emitAssets: consumer === 'client',
    createEnvironment: (name, config) => new BuildEnvironment(name, config),
  }, raw)

  // target 的语义化展开
  if (merged.target === 'baseline-widely-available') {
    merged.target = ESBUILD_BASELINE_WIDELY_AVAILABLE_TARGET
  }
  if (Array.isArray(merged.target)) {
    merged.target = unique(merged.target)  // 去重(oxc 不允许重复)
  }

  // minify 的规范化
  if ((merged.minify as string) === 'false') merged.minify = false
  else if (merged.minify === true) merged.minify = 'oxc'

  return resolved
}

consumer 参数是这个函数中最巧妙的设计之一。它根据构建环境的消费者类型(浏览器客户端还是服务端)自动调整默认值:客户端默认启用压缩和资源输出,服务端默认禁用压缩且不输出资源文件。这种基于角色的默认值策略大大减少了用户需要手动配置的选项。

下面的图表展示了各个关键配置选项的默认值,以及它们如何随构建环境类型变化:

graph LR subgraph "默认值策略(按环境类型)" A["target"] --> A1["'baseline-widely-available'
Chrome 111+, Firefox 114+, Safari 16.4+"] B["minify"] --> B1["client: 'oxc' (Rust 压缩器)
server: false (不压缩)"] C["outDir"] --> C1["'dist'"] D["assetsDir"] --> D1["'assets'"] E["assetsInlineLimit"] --> E1["4096 字节 (4KB)"] F["sourcemap"] --> F1["false"] G["cssCodeSplit"] --> G1["非 lib 模式: true
lib 模式: false"] H["emitAssets"] --> H1["client: true (输出资源)
其他环境: false"] end

构建目标的语义化

baseline-widely-available 是 Vite 引入的一个语义化目标值。它代表 "在 2026-01-01 之前被广泛支持的浏览器基线",会被展开为具体的浏览器版本列表。这种语义化的目标定义比直接指定 ['chrome111', 'firefox114', 'safari16.4'] 更加直观和可维护。去重处理是从 Rollup 到 Rolldown 迁移带来的需求------esbuild 允许目标列表中的重复项,但 oxc 压缩器不允许。

13.4 Rolldown 选项构建

resolveRolldownOptions:配置生成的核心

这是整个构建流程中最关键的配置生成函数。它将 Vite 的高层抽象配置翻译为 Rolldown 可以直接消费的底层选项。每一个配置项的选择都蕴含着对不同构建场景的深入考量:

typescript 复制代码
export function resolveRolldownOptions(
  environment: Environment,
  chunkMetadataMap: ChunkMetadataMap,
): RolldownOptions {
  const { root, packageCache, build: options } = environment.config
  const ssr = environment.config.consumer === 'server'

  // 1. 确定入口------不同模式有不同的入口解析逻辑
  const resolve = (p: string) => path.resolve(root, p)
  const input = libOptions
    ? options.rollupOptions.input || /* 库模式:从 lib.entry 解析 */
    : typeof options.ssr === 'string'
      ? resolve(options.ssr)           // SSR:使用指定的服务端入口
      : options.rollupOptions.input || resolve('index.html')  // 应用模式:默认 index.html

  // 2. 注入环境上下文到插件钩子
  const plugins = environment.plugins.map((p) =>
    injectEnvironmentToHooks(environment, chunkMetadataMap, p),
  )

  // 3. 构建完整的 Rolldown 选项
  const rollupOptions: RolldownOptions = {
    preserveEntrySignatures: ssr ? 'allow-extension' : libOptions ? 'strict' : false,
    ...options.rollupOptions,
    input,
    plugins,
    onLog(level, log) { onRollupLog(level, log, environment) },
    transform: {
      target: options.target === false ? undefined : options.target,
      define: {
        ...options.rollupOptions.transform?.define,
        // 禁用 Rolldown 内置的 process.env.NODE_ENV 替换
        // 因为 Vite 的 define 插件会处理这个
        'process.env.NODE_ENV': 'process.env.NODE_ENV',
      },
    },
    // 暂时由 Vite 的 CSS 插件处理 CSS,告诉 Rolldown 将 .css 视为 JS
    moduleTypes: { '.css': 'js' },
    // 启用 Vite 模式,激活 Rolldown 中的 Vite 特定行为
    experimental: { viteMode: true },
  }

  return rollupOptions
}

moduleTypes: { '.css': 'js' } 这个看似奇怪的配置值得解释。Rolldown 有内置的 CSS 处理能力,但 Vite 目前仍然通过自己的 CSS 插件管线来处理样式文件------因为 Vite 的 CSS 处理包含了 PostCSS 集成、CSS Modules、预处理器支持等 Rolldown 原生不提供的功能。通过将 .css 文件类型映射为 js,Vite 的 CSS 插件可以在 transform 钩子中将 CSS 转换为 JavaScript 代码,而 Rolldown 不会对这些文件应用自己的 CSS 处理逻辑。

preserveEntrySignatures 的三种策略

入口签名的保留策略根据构建模式有所不同。SSR 使用 allow-extension------保留入口的原始导出,但允许插件添加额外导出(例如注入服务端渲染所需的辅助函数)。库模式使用 strict------严格保留入口的导出签名不变,确保库的公共 API 不被构建过程修改。应用模式使用 false------完全不保留入口签名,允许 Rolldown 自由地合并和优化入口模块。

输出选项构建

buildOutputOptions 函数为 Rolldown 的输出阶段生成配置。文件命名策略是这里最重要的决策------它直接影响产物的缓存行为和部署方式:

typescript 复制代码
const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => {
  const format = output.format || 'es'
  const jsExt = (ssr && !isSsrTargetWebworkerEnvironment) || libOptions
    ? resolveOutputJsExtension(format, packageData?.type)
    : 'js'

  return {
    dir: outDir,
    format,
    exports: 'auto',
    sourcemap: options.sourcemap,

    // 文件命名策略------不同场景使用不同的命名模式
    entryFileNames: ssr
      ? `[name].${jsExt}`                    // SSR: 简洁名称,无 hash
      : libOptions
        ? ({ name }) => resolveLibFilename(libOptions, format, name, root, jsExt)  // 库:自定义命名
        : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),            // 应用:带 hash

    chunkFileNames: libOptions
      ? `[name]-[hash].${jsExt}`             // 库的分块:带 hash 但不在 assets 目录下
      : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),

    assetFileNames: libOptions
      ? `[name].[ext]`                       // 库的资源:保持原名
      : path.posix.join(options.assetsDir, `[name]-[hash].[ext]`),

    // Rolldown 的压缩配置
    minify: options.minify === 'oxc'
      ? (libOptions && format === 'es' ? { compress: true, mangle: true, codegen: false } : true)
      : false,
    topLevelVar: true,
    ...output,
  }
}
graph TD subgraph "文件命名策略" A["SSR 构建"] --> A1["入口: [name].js / .mjs / .cjs
(无 hash,无 assets 目录)"] B["库模式"] --> B1["入口: resolveLibFilename() 计算
分块: [name]-[hash].js
资源: [name].[ext](无 hash)"] C["应用模式"] --> C1["入口: assets/[name]-[hash].js
分块: assets/[name]-[hash].js
资源: assets/[name]-[hash].[ext]"] end style A1 fill:#e1f5fe style B1 fill:#fff3e0 style C1 fill:#e8f5e9

对于 ES 格式的库,压缩配置使用了一个特殊组合:{ compress: true, mangle: true, codegen: false }codegen: false 的含义是不压缩空白------这对于 ES 库非常重要,因为很多 tree-shaking 工具依赖 /* @__PURE__ */ 注解来判断函数调用是否有副作用,而空白压缩可能破坏这些注解的位置关系。

JS 扩展名解析

Node.js 的模块系统根据文件扩展名和 package.jsontype 字段共同决定文件的模块格式。Vite 的扩展名解析逻辑确保了输出文件在 Node.js 中能被正确识别。当 package.json 声明 "type": "module" 时,.js 文件被视为 ES Module,CJS 格式需要使用 .cjs 扩展名;反之,.js 被视为 CommonJS,ES Module 需要使用 .mjs

13.5 构建执行引擎

buildEnvironment:核心打包逻辑

这是实际执行 Rolldown 打包的函数。它的结构清晰地反映了构建的三个阶段:准备(配置解析)、执行(打包)、收尾(元数据注入和资源清理)。错误处理包裹了整个流程,确保即使构建失败,bundle 资源也会被正确释放:

typescript 复制代码
async function buildEnvironment(
  environment: BuildEnvironment,
): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
  const { logger, config } = environment

  logger.info(
    colors.cyan(`vite v${VERSION} building ${environment.name} for ${config.mode}...`),
  )

  let bundle: RolldownBuild | undefined
  let startTime: number | undefined
  try {
    const chunkMetadataMap = new ChunkMetadataMap()
    const rollupOptions = resolveRolldownOptions(environment, chunkMetadataMap)

    // Watch 模式:启动文件监听,增量重建
    if (options.watch) {
      const { watch } = await import('rolldown')
      const watcher = watch({ ...rollupOptions, watch: { ...options.watch } })
      watcher.on('event', (event) => {
        if (event.code === 'BUNDLE_START') {
          chunkMetadataMap.clearResetChunks()  // 重置 chunk 元数据
        } else if (event.code === 'BUNDLE_END') {
          event.result.close()
        } else if (event.code === 'ERROR') {
          enhanceRollupError(event.error)
          logger.error(event.error.message)
        }
      })
      return watcher
    }

    // 正常构建:一次性打包
    const { rolldown } = await import('rolldown')
    startTime = Date.now()
    bundle = await rolldown(rollupOptions)

    // 执行输出------可能有多个输出配置(例如库模式的 ES + CJS)
    const res: RolldownOutput[] = []
    for (const output of arraify(rollupOptions.output!)) {
      res.push(await bundle[options.write ? 'write' : 'generate'](output))
    }

    // 为每个 chunk 注入 Vite 元数据
    for (const output of res) {
      for (const chunk of output.output) {
        injectChunkMetadata(chunkMetadataMap, chunk)
      }
    }

    logger.info(`${colors.green(`built in ${displayTime(Date.now() - startTime)}`)}`)
    return Array.isArray(rollupOptions.output) ? res : res[0]
  } catch (e) {
    enhanceRollupError(e)
    throw e
  } finally {
    if (bundle) await bundle.close()
  }
}
sequenceDiagram participant Env as BuildEnvironment participant Opts as resolveRolldownOptions participant RD as Rolldown (Rust) participant Plugins as 插件系统 participant FS as 文件系统 Env->>Opts: 构建 Rolldown 选项 Opts->>Opts: 解析入口、插件、输出配置 Opts-->>Env: RolldownOptions Env->>RD: rolldown(options) 启动打包 Note right of RD: Rust 层执行
模块解析
依赖图构建
Tree-shaking RD->>Plugins: resolveId / load / transform Plugins-->>RD: 转换后的代码 RD-->>Env: RolldownBuild (bundle 对象) loop 每个输出配置 Env->>RD: bundle.write(output) RD->>Plugins: renderChunk (替换占位符) Plugins-->>RD: 处理后的 chunk 代码 RD->>Plugins: generateBundle (最终调整) Plugins-->>RD: 确认输出 RD->>FS: 写入产物文件 RD-->>Env: RolldownOutput end Env->>Env: 注入 chunk 元数据 (viteMetadata) Env->>RD: bundle.close() 释放资源

13.6 ChunkMetadata:构建元数据管理

为什么需要 ChunkMetadataMap

Rolldown 的 chunk 对象只包含标准的打包信息------代码、导入关系、导出列表等。但 Vite 的 HTML 插件、CSS 插件、manifest 插件等需要知道每个 chunk 关联了哪些 CSS 文件和静态资源。ChunkMetadataMap 就是为了桥接这一信息差距而设计的。

它使用 preliminaryFileName(初步文件名)作为键来关联 chunk 和元数据。选择初步文件名而非最终文件名是因为在 renderChunk 阶段------元数据最常被访问的时候------最终文件名可能还未确定:

typescript 复制代码
export class ChunkMetadataMap {
  private _inner = new Map<string, ChunkMetadata | AssetMetadata>()
  private _resetChunks = new Set<string>()

  private _getKey(chunk): string {
    return 'preliminaryFileName' in chunk ? chunk.preliminaryFileName : chunk.fileName
  }

  private _getDefaultValue(chunk): ChunkMetadata | AssetMetadata {
    return chunk.type === 'chunk'
      ? {
          importedAssets: new Set(),
          importedCss: new Set(),
          __modules: chunk.modules,  // 共享引用,允许 JS 侧插件修改
        }
      : { importedAssets: new Set(), importedCss: new Set() }
  }

  get(chunk): ChunkMetadata | AssetMetadata {
    const key = this._getKey(chunk)
    if (!this._inner.has(key)) {
      this._inner.set(key, this._getDefaultValue(chunk))
    }
    return this._inner.get(key)!
  }
}

元数据注入的精妙细节

injectChunkMetadata 函数使用 Object.defineProperty 而非直接赋值来将元数据注入 chunk 对象。这个选择不是编码风格偏好,而是解决了一个具体的技术问题:Rolldown 对输出对象有变更追踪机制,直接赋值新属性可能触发不必要的内部处理,而 defineProperty 可以更精确地控制属性的行为:

typescript 复制代码
function injectChunkMetadata(chunkMetadataMap, chunk, resetChunkMetadata = false) {
  if (resetChunkMetadata) chunkMetadataMap.reset(chunk)

  Object.defineProperty(chunk, 'viteMetadata', {
    value: chunkMetadataMap.get(chunk),
    enumerable: true,
  })

  // 让 chunk.modules 通过 viteMetadata 间接访问
  if (chunk.type === 'chunk') {
    Object.defineProperty(chunk, 'modules', {
      get() { return chunk.viteMetadata!.__modules },
      enumerable: true,
    })
  }
}

13.7 环境注入与插件隔离

injectEnvironmentToHooks:为插件注入上下文

Rolldown 的插件系统不原生支持 Vite 的多环境概念。每个插件钩子被调用时,它的 this 上下文是一个标准的 PluginContext,不包含环境信息。injectEnvironmentToHooks 通过包装每个钩子函数来注入环境上下文。它创建了插件的浅拷贝(保持原型链),然后逐个替换钩子函数为包装版本:

typescript 复制代码
export function injectEnvironmentToHooks(
  environment: Environment,
  chunkMetadataMap: ChunkMetadataMap,
  plugin: Plugin,
): Plugin {
  const clone = Object.assign(Object.create(Object.getPrototypeOf(plugin)), plugin)

  for (const hook of Object.keys(clone) as RollupPluginHooks[]) {
    switch (hook) {
      case 'resolveId':
        clone[hook] = wrapEnvironmentResolveId(environment, resolveId, plugin.name)
        break
      case 'load':
        clone[hook] = wrapEnvironmentLoad(environment, load, plugin.name)
        break
      case 'transform':
        clone[hook] = wrapEnvironmentTransform(environment, transform, plugin.name)
        break
      default:
        if (ROLLUP_HOOKS.includes(hook)) {
          clone[hook] = wrapEnvironmentHook(environment, chunkMetadataMap, plugin, hook)
        }
        break
    }
  }
  return clone
}

resolveIdloadtransform 有专门的包装函数,因为它们除了注入环境之外还需要注入 ssr 标志。其他标准的 Rollup 钩子使用通用的 wrapEnvironmentHook,其中 renderChunkgenerateBundle 等钩子还有额外的元数据注入逻辑。

SSR 标志的废弃路径

Vite 正在逐步推动生态系统从使用 options.ssr 标志转向使用 this.environment 来判断当前环境。这个迁移通过 getter 陷阱实现了优雅的过渡------当插件访问 options.ssr 时,如果启用了废弃警告,会提示开发者使用新的 API:

typescript 复制代码
function injectSsrFlag(options, environment, pluginName) {
  let ssr = environment.config.consumer === 'server'
  const newOptions = { ...options, ssr }

  if (isFutureDeprecationEnabled(config, 'removePluginHookSsrArgument')) {
    Object.defineProperty(newOptions, 'ssr', {
      get() {
        warnFutureDeprecation(config, 'removePluginHookSsrArgument',
          `Used in plugin "${pluginName}".`)
        return ssr
      },
    })
  }
  return newOptions
}

13.8 多环境构建架构

ViteBuilder:统一的构建编排

多环境构建是现代 Web 应用的常见需求。一个全栈应用可能需要同时构建浏览器端代码(客户端渲染)、服务端代码(SSR)、甚至 Web Worker 代码。Vite 的 ViteBuilder 接口提供了统一的编排机制来管理这些并行的构建任务:

typescript 复制代码
export interface ViteBuilder {
  environments: Record<string, BuildEnvironment>
  config: ResolvedConfig
  buildApp(): Promise<void>
  build(environment: BuildEnvironment): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher>
}

createBuilder:构建器的创建过程

createBuilder 是多环境构建的核心编排函数。它负责解析配置、创建环境实例、提供构建 API。最值得深入研究的是它如何处理环境间的配置隔离与插件共享:

graph TD A["createBuilder(inlineConfig)"] --> B{"useLegacyBuilder?"} B -->|"是(向后兼容模式)"| C["单环境模式"] C --> C1["setupEnvironment('client', config)"] C1 --> C2["new BuildEnvironment('client', config)"] B -->|"否(新的多环境模式)"| D["多环境模式"] D --> D1["遍历 config.environments 中定义的所有环境"] D1 --> D2{"sharedConfigBuild?"} D2 -->|是| D3["所有环境共享同一个 config 实例"] D2 -->|否| D4["为每个环境独立解析 config"] D4 --> D5["resolveConfigToBuild()
patchConfig: 覆盖 config.build
patchPlugins: 处理共享插件"] D3 --> E["并行初始化所有 BuildEnvironment"] D5 --> E E --> E1["BuildEnvironment('client')"] E --> E2["BuildEnvironment('ssr')"] E --> E3["BuildEnvironment('worker')"] style A fill:#e1f5fe style E1 fill:#e8f5e9 style E2 fill:#fff3e0 style E3 fill:#fce4ec

buildApp:智能的构建编排

buildApp 方法的实现展示了 Vite 的构建钩子系统。它按 pre/normal/post 顺序执行所有插件的 buildApp 钩子,在适当的时机插入 config.builder.buildApp 的执行。如果所有钩子执行完毕后没有任何环境被标记为已构建,系统会自动回退到顺序构建所有环境------这是一个优秀的默认行为,确保即使没有自定义的构建编排逻辑,所有环境也能被正确构建:

typescript 复制代码
async buildApp() {
  for (const p of config.getSortedPlugins('buildApp')) {
    const hook = p.buildApp
    if (!configBuilderBuildAppCalled && typeof hook === 'object' && hook.order === 'post') {
      configBuilderBuildAppCalled = true
      await configBuilder.buildApp(builder)
    }
    await handler.call(pluginContext, builder)
  }
  // 回退策略:如果没有环境被构建,构建所有环境
  if (Object.values(builder.environments).every((env) => !env.isBuilt)) {
    for (const environment of Object.values(builder.environments)) {
      await builder.build(environment)
    }
  }
}

独立配置 vs 共享配置

多环境构建面临一个核心的架构抉择:环境间应该共享配置和插件实例,还是各自独立?默认情况下 Vite 选择了独立配置(sharedConfigBuild: false),这看似冗余实则必要。

原因在于生态系统的现状:大多数 Rollup/Vite 插件在设计时假设自己只处理一个 bundle。它们在内部维护状态(缓存、计数器、已处理文件列表等),这些状态在处理完一个 bundle 后可能处于 "脏" 状态。如果共享插件实例,一个环境的构建残留可能影响另一个环境。

但标记了 sharedDuringBuild: true 的插件会被显式共享,这允许某些需要跨环境协调的插件(如资源去重、共享 CSS 处理等)在多个环境间复用同一个实例。

13.9 构建插件体系

Vite 的构建插件分为 pre 和 post 两组,它们分别在用户插件之前和之后执行。这种分层确保了内置功能不被用户插件意外干扰,同时用户插件也能在正确的时机介入:

graph TD subgraph "Pre 插件(在用户插件之前)" P1["prepareOutDirPlugin
清理和准备输出目录"] P2["vite:rollup-options-plugins
执行用户的 Rolldown 插件"] P3["webWorkerPostPlugin
Worker 脚本处理"] end subgraph "核心插件(用户配置的插件在此区间)" M1["vite:resolve (模块解析)"] M2["vite:html (HTML 转换)"] M3["vite:css (样式处理)"] M4["vite:asset (资源处理)"] M5["vite:define (常量替换)"] end subgraph "Post 插件(在用户插件之后)" O1["buildImportAnalysisPlugin
动态导入分析与 preload"] O2["terserPlugin
可选的 Terser 压缩"] O3["licensePlugin
第三方许可证收集"] O4["manifestPlugin
资源清单生成"] O5["ssrManifestPlugin
SSR 清单生成"] O6["buildReporterPlugin
构建报告(大小统计)"] O7["nativeLoadFallbackPlugin
Rolldown 原生加载回退"] end P1 --> P2 --> P3 --> M1 --> M2 --> M3 --> M4 --> M5 --> O1 --> O2 --> O3 --> O4 --> O5 --> O6 --> O7

13.10 库模式构建

库模式是 Vite 构建系统的一个重要用例。与应用模式不同,库模式需要生成可被其他项目 import 的包,因此在文件命名、模块格式、入口签名等方面有特殊要求。

多格式输出

库默认生成 ES 和 UMD 两种格式(多入口库默认生成 ES 和 CJS)。resolveBuildOutputs 函数将用户的库配置展开为 Rolldown 的多输出配置。UMD 和 IIFE 格式有额外的限制:不支持多入口(因为它们需要一个全局变量名),且必须提供 name 选项:

typescript 复制代码
export function resolveBuildOutputs(outputs, libOptions, logger) {
  if (libOptions) {
    const libHasMultipleEntries = typeof libOptions.entry !== 'string' &&
      Object.values(libOptions.entry).length > 1
    const libFormats = libOptions.formats ||
      (libHasMultipleEntries ? ['es', 'cjs'] : ['es', 'umd'])

    if (libFormats.includes('umd') || libFormats.includes('iife')) {
      if (libHasMultipleEntries) {
        throw new Error('Multiple entry points are not supported for "umd" or "iife" formats.')
      }
      if (!libOptions.name) {
        throw new Error('"build.lib.name" is required for "umd" or "iife" formats.')
      }
    }
    return libFormats.map((format) => ({ ...outputs, format }))
  }
  return outputs
}

库文件命名

库的文件命名需要考虑 package.json 的 name 字段、输出格式和文件扩展名。resolveLibFilename 函数实现了这个复杂的命名逻辑。它支持函数形式的 fileName,让库作者完全控制输出文件名。对于 ES 和 CJS 格式,文件名为 name.ext;对于 UMD 和 IIFE,文件名为 name.format.ext,以区分不同格式的输出。

13.11 错误处理与日志

增强的错误信息

enhanceRollupError 函数将 Rolldown 的原始错误增强为开发者友好的格式。它添加了彩色的插件名称标识、精确的文件位置信息(包含行号和列号)和格式化的代码帧(高亮错误位置的周围代码)。重建堆栈信息的逻辑确保了增强后的错误消息出现在堆栈的顶部,因为 JavaScript 不保证修改 e.messagee.stack 会自动更新。

日志分级与警告过滤

构建过程中的日志通过 onRollupLog 函数统一处理。它实现了几个重要的过滤规则:CIRCULAR_DEPENDENCY(循环依赖)和 THIS_IS_UNDEFINED(this 值为 undefined)被静默忽略,因为这些在现代 JavaScript 项目中非常常见且通常无害。UNRESOLVED_IMPORT(未解析的导入)则被提升为错误------除非它是一个 CommonJS 的外部依赖,因为那是正常的行为。

13.12 产物输出路径系统

相对路径与运行时计算

对于使用相对路径 base(base: './')的项目,资源路径需要在运行时根据当前模块的位置动态计算。不同的模块格式使用不同的运行时机制来获取当前模块的 URL,然后计算资源的相对路径。ES Module 使用 new URL(path, import.meta.url).href,CJS 则需要同时处理 Node.js(pathToFileURL(__dirname + '/' + path))和浏览器(document.currentScript.src)两种环境:

typescript 复制代码
const relativeUrlMechanisms = {
  es: (relativePath) =>
    getResolveUrl(`'${escapeId(relativePath)}', import.meta.url`),
  cjs: (relativePath) =>
    `(typeof document === 'undefined' ? ${getFileUrlFromRelativePath(relativePath)} : ${getRelativeUrlFromDocument(relativePath)})`,
  iife: (relativePath) =>
    getRelativeUrlFromDocument(relativePath),
  umd: (relativePath) =>
    `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath(relativePath)} : ${getRelativeUrlFromDocument(relativePath, true)})`,
}

这些运行时代码片段会被注入到每个引用了资源路径的 chunk 中,替换构建时的占位符。

13.13 设计决策分析

为什么每个环境默认独立解析配置

这个决策反映了 Vite 团队对生态系统现状的务实认知。虽然独立解析意味着多环境构建需要多次解析配置(有一定的性能开销),但它保证了插件的状态隔离------这在当前大多数插件还没有为多环境设计的情况下是必要的。sharedConfigBuildsharedPlugins 选项为确信自己的插件是多环境安全的项目提供了优化路径。

为什么使用 oxc 而非 esbuild 作为默认压缩器

这是技术栈统一的考量。oxc 是 Rolldown 生态的一部分,与 Rolldown 共享 Rust 基础设施。使用 oxc 意味着不需要启动额外的子进程(esbuild 是 Go 编写的独立进程),消除了进程间通信的开销。而且 oxc 作为 Rolldown 的一部分可以直接访问 AST 信息,避免了序列化和反序列化的成本。

为什么 experimental.viteMode

experimental: { viteMode: true } 这个标志告诉 Rolldown 当前是在 Vite 模式下运行。这启用了 Rolldown 中为 Vite 定制的行为路径,例如 viteMetadata 的处理和特定的模块类型推断逻辑。这是 Vite 和 Rolldown 深度集成的体现------两个项目不是简单的调用关系,而是在底层有着紧密的协作。

Watch 模式中的 chunk 元数据重置

Watch 模式下,BUNDLE_START 事件触发时调用 chunkMetadataMap.clearResetChunks()。这个看似细微的操作解决了一个重要问题:在增量重建中,某些 chunk 的名称可能保持不变但内容已更新。如果不重置元数据,旧的 CSS 文件列表和资源列表可能残留到新的构建结果中,导致 HTML 中注入了错误的资源引用。

13.14 小结

Vite 的构建引擎是一个精心编排的多层架构,每一层都有清晰的职责和边界:

  1. 入口层build() / createBuilder() 提供简洁的公共 API,隐藏内部复杂性
  2. 配置层resolveBuildEnvironmentOptions() / resolveRolldownOptions() 将高层语义翻译为底层选项
  3. 编排层ViteBuilder / buildApp() 管理多环境构建的执行顺序和资源共享
  4. 执行层buildEnvironment() 驱动 Rolldown 完成实际的打包工作
  5. 元数据层ChunkMetadataMap / injectEnvironmentToHooks() 在 Rolldown 和 Vite 插件之间搭建信息桥梁

从 Rollup 到 Rolldown 的迁移不仅是一次性能升级,更是 Vite 技术战略的关键落子。Rolldown 作为 Rust 实现的打包器,与 Vite 的其他 Rust 组件形成了统一的工具链。这意味着未来模块解析、代码转换、打包和压缩可以在同一个 Rust 进程中协作完成,避免了 JavaScript 和原生代码之间的频繁切换。

多环境构建架构则体现了 Vite 团队对现代 Web 应用复杂性的深刻理解。一个应用可能需要同时为浏览器、Node.js 服务端、Web Worker 等多个运行时生成优化的产物。统一的 Builder API 在概念上保持简洁------所有环境共享相同的构建管线------在实现上保持灵活------每个环境可以有独立的配置、插件和输出策略。这种简洁与灵活的平衡是优秀架构设计的标志。

相关推荐
杨艺韬4 小时前
Vite内核解析-第18章 设计模式与架构决策
agent
杨艺韬4 小时前
Vite内核解析-第4章 插件系统与 Hook 机制
agent
杨艺韬4 小时前
Vite内核解析-第6章 模块图与依赖追踪
agent
杨艺韬4 小时前
Vite内核解析-第11章 HTML 转换与入口解析
agent
杨艺韬4 小时前
Vite内核解析-前言
agent
杨艺韬4 小时前
Vite内核解析-第7章 HMR 热更新
agent
杨艺韬4 小时前
Vite内核解析-第9章 JavaScript 与 TypeScript 转换
agent
杨艺韬4 小时前
Vite内核解析-第1章 为什么需要理解 Vite
agent
杨艺韬4 小时前
Vite内核解析-第8章 依赖预构建
agent