Read OSS

生产构建、ViteBuilder 与模块运行器

高级

前置知识

  • 第 1 篇:架构与代码库导览
  • 第 2 篇:配置与环境系统
  • 第 3 篇:开发服务器与转换流水线
  • 第 4 篇:插件系统与核心插件
  • 第 5 篇:HMR 与依赖优化

生产构建、ViteBuilder 与模块运行器

Vite 的开发模式与生产模式在本质上截然不同。开发时,模块在浏览器请求时按需转换;生产时,所有内容都会被打包、tree-shaking、压缩,最终写入磁盘。但两种模式共享同一套插件系统和大部分配置逻辑。

本系列的最后一篇文章将介绍三个剩余子系统:通过 Rolldown 生成优化产物的 build() 流水线、协调多环境构建的 ViteBuilder,以及可在任意 JavaScript 运行时中执行服务端代码的 ModuleRunner

build() 函数与 BuildEnvironment

生产构建从 build() 函数开始。如第 1 篇所述,该函数由 CLI 通过 createBuilder 调用,实际工作在 buildEnvironment 中完成:

sequenceDiagram
    participant CLI
    participant createBuilder
    participant buildEnvironment
    participant Rolldown

    CLI->>createBuilder: createBuilder(inlineConfig)
    createBuilder->>createBuilder: resolveConfig(inlineConfig, 'build')
    createBuilder->>createBuilder: Create BuildEnvironment per env
    createBuilder->>buildEnvironment: builder.build(environment)
    buildEnvironment->>buildEnvironment: resolveRolldownOptions(environment)
    buildEnvironment->>Rolldown: rolldown(rollupOptions)
    Rolldown-->>buildEnvironment: RolldownBuild
    buildEnvironment->>Rolldown: bundle.write(outputOptions)
    Rolldown-->>buildEnvironment: RolldownOutput
    buildEnvironment-->>CLI: Output files

BuildEnvironment 类继承自 BaseEnvironment,新增了 mode: 'build' 标志和 isBuilt 追踪器。相比 DevEnvironment,它的设计更为简洁——没有模块图、没有热更新通道、也不需要追踪待处理的请求。

resolveRolldownOptions 函数负责将 Vite 配置转换为 Rolldown 的 RolldownOptions,包括从 HTML 文件(应用模式)或配置(库模式)中解析入口点、设置输出选项(格式、chunk 命名、资源内联阈值),以及集成插件流水线。

第 792–860 行buildEnvironment 函数调用 rolldown() 创建构建实例,随后在普通构建中调用 bundle.write(),或在 --watch 模式下启动文件监听器。Watch 模式使用 Rolldown 原生的文件监听器,chokidar 选项由 Vite 配置解析。

ViteBuilder 与多环境构建

createBuilder 函数创建一个 ViteBuilder 实例,用于管理多个环境的构建。核心方法是 第 1807–1841 行buildApp()

sequenceDiagram
    participant Framework
    participant buildApp
    participant Plugins
    participant configBuilder

    buildApp->>Plugins: Run 'pre' and 'normal' buildApp hooks
    Plugins-->>buildApp: (may build some environments)
    buildApp->>configBuilder: configBuilder.buildApp(builder)
    configBuilder-->>buildApp: (default: no-op)
    buildApp->>Plugins: Run 'post' buildApp hooks
    buildApp->>buildApp: Any environments not built?
    alt Some environments unbuilt
        buildApp->>buildApp: Build remaining environments sequentially
    end

buildApp 插件钩子是框架控制构建编排的核心机制。像 Nuxt 或 SolidStart 这样的元框架可以利用这个钩子实现以下流程:

  1. 优先构建客户端环境
  2. 读取客户端 manifest 文件
  3. 以客户端 chunk 为参照,构建 SSR 环境
  4. 协调各环境的输出目录

该钩子与其他钩子一样遵循 enforce/order 排序规则:order: 'pre' 和普通钩子优先执行,接着是 configBuilder.buildApp(用户的 builder 配置),最后是 order: 'post' 钩子。

第 1832–1840 行 有一段兜底逻辑:如果没有任何 buildApp 钩子触发构建,则按顺序依次构建所有环境:

if (Object.values(builder.environments).every(
  (environment) => !environment.isBuilt,
)) {
  for (const environment of Object.values(builder.environments)) {
    await builder.build(environment)
  }
}

为了保证各环境配置相互隔离,createBuilder 默认会为每个环境单独解析配置(即 sharedConfigBuild 为 false 时)。这样每次构建都能获得全新的插件实例,符合生态系统中"插件每次只处理一个 bundle"的惯例。

构建时的导入分析

buildImportAnalysisPlugin 的处理方式与开发模式下的同名插件有所不同。开发时,导入语句会被改写以添加时间戳查询参数,裸模块导入也会被重定向到预优化的依赖;构建时,重点则转向以下方面:

flowchart TD
    A["Parse imports with es-module-lexer"] --> B{"Dynamic import?"}
    B -->|Yes| C["Insert __vitePreload wrapper"]
    B -->|No| D["Leave as static import"]
    C --> E["Collect CSS deps for preload"]
    E --> F["Generate preload directive"]
    F --> G["Replace __VITE_PRELOAD__ marker in generateBundle"]

预加载系统对性能至关重要。当 chunk A 动态导入 chunk B,而 chunk B 又引用了一个 CSS 文件时,预加载指令会确保该 CSS 文件与 chunk B 并行加载,而非瀑布式加载。__VITE_PRELOAD__ 占位符在 transform 阶段插入,等到 generateBundle 阶段完整的 chunk 图可知时,再替换为真实的 chunk 路径。

该构建插件还会将部分可以在 Rust 中运行的分析工作,委托给原生的 Rolldown 插件(来自 rolldown/experimentalviteBuildImportAnalysisPlugin)处理。

ModuleRunner 系统

ModuleRunner 是 Vite 为在任意 JavaScript 运行时中执行服务端代码而设计的方案,是 SSR 的基础,可运行于 Node.js、Worker 或边缘运行时。

graph TD
    subgraph "ModuleRunner"
        MR["ModuleRunner"] --> EM["EvaluatedModules<br/>(module cache)"]
        MR --> TR["Transport<br/>(to DevEnvironment)"]
        MR --> HMR["HMRClient<br/>(optional)"]
        MR --> EV["ModuleEvaluator"]
    end

    subgraph "ESModulesEvaluator"
        EV --> AF["new AsyncFunction(<br/>  __vite_ssr_exports__,<br/>  __vite_ssr_import_meta__,<br/>  __vite_ssr_import__,<br/>  ...<br/>  code<br/>)"]
    end

    TR -->|"fetchModule(id)"| DEV["DevEnvironment<br/>(Vite Server)"]
    DEV -->|"transform + ssrTransform"| TR

第 53–79 行 的构造函数接受选项、可选的执行器(默认为 ESModulesEvaluator)以及可选的调试器。若启用了 HMR,则会使用 src/shared/hmr.ts 中共享的 HMRClient 类(与浏览器客户端使用的是同一个类)创建 HMRClient 实例。

ESModulesEvaluator 使用 AsyncFunction 来执行转换后的代码:

const initModule = new AsyncFunction(
  ssrModuleExportsKey,
  ssrImportMetaKey,
  ssrImportKey,
  ssrDynamicImportKey,
  ssrExportAllKey,
  ssrExportNameKey,
  '"use strict";' + code,
)

选择 AsyncFunction 而非 vm.runInNewContext,是因为前者可以在任何 JavaScript 环境中运行——浏览器、Deno、Cloudflare Workers 均可——而不局限于 Node.js。模块导出在求值完成后会通过 Object.seal() 封冻,以防止意外修改。

提示: ESModulesEvaluator 上的 startOffset 属性用于补偿 AsyncFunction 包装器引入的行偏移量,确保模块求值过程中抛出错误时,source map 能够正确对齐。

传输层与跨环境通信

ModuleRunnerTransport 接口抽象了模块运行器与 Vite 服务器之间的通信方式:

export interface ModuleRunnerTransport {
  connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
  disconnect?(): Promise<void> | void
  send?(data: HotPayload): Promise<void> | void
  invoke?(data: HotPayload): Promise<{ result: any } | { error: any }>
  timeout?: number
}

这里有两种通信模型:send/connect(基于消息,适用于 WebSocket 或 Worker 消息)和 invoke(RPC 风格,适用于同进程通信)。normalizeModuleRunnerTransport 函数会将任意一种模型统一包装为带有类型化 RPC 方法的 InvokeableModuleRunnerTransport

graph LR
    subgraph "Same Process"
        RUNNER1["ModuleRunner"] -->|"invoke()"| DEV1["DevEnvironment"]
    end
    subgraph "Worker Thread"
        RUNNER2["ModuleRunner"] -->|"send/connect"| MSG["MessagePort"] -->|"send/connect"| DEV2["DevEnvironment"]
    end
    subgraph "Remote (Edge)"
        RUNNER3["ModuleRunner"] -->|"send/connect"| WS["WebSocket"] -->|"send/connect"| DEV3["DevEnvironment"]
    end

对于 send/connect 模型,第 43–80 行 的传输实现基于 nanoid 生成的请求 ID 和一个存储待处理 Promise 的 Map 来构建 RPC 层,响应通过 ID 匹配,并支持配置超时时间。

开发模式 vs. 生产模式:对比一览

作为本系列的收尾,我们来看看模块在 Vite 两种模式下的完整流转对比:

维度 开发模式(transformRequest) 生产模式(Rolldown bundle)
入口 浏览器 HTTP 请求 配置入口点 / HTML
解析 插件容器 resolveId Rolldown 原生 + 插件 resolveId
加载 插件容器 load → 文件系统兜底 Rolldown 原生 + 插件 load
转换 插件容器 transform(顺序执行) Rolldown + 插件 transform(并行执行)
输出 单个模块响应 打包后的 chunk,经过 tree-shaking
模块图 EnvironmentModuleGraph(各环境独立) Rolldown 内部图
导入处理 importAnalysisPlugin 改写 buildImportAnalysisPlugin 解析
CSS 通过 <style> 标签注入 提取为独立 .css 文件
依赖 由优化器预打包 内联打包或分包处理
HMR WebSocket → 重新导入 不适用(watch 模式触发重新构建)

两种模式的交汇点在于插件系统。相同的插件运行于两种模式,接收相同的钩子调用。hotUpdate 等 Vite 专属钩子天然只在开发模式下生效,generateBundle 等构建专属钩子只在生产构建时触发。但核心的 resolveIdloadtransform 流水线是共享的。

随着 Vite 8 迁移至 Rolldown,开发与生产之间的差距进一步缩小。Rolldown 的原生解析器同时驱动两种模式。第 5 篇介绍的实验性全量打包开发模式更是彻底抹平了这一界限——开发时直接提供打包产物,与生产模式别无二致。

系列总结

历经六篇文章,我们从 79 行的 CLI 入口出发,依次深入 Vite 8 的配置解析、18 层中间件栈、拥有 28+ 个核心插件的插件系统、HMR 边界传播、依赖预打包、生产构建,直至模块运行器。核心要点如下:

  1. 四个运行时上下文(node、client、module-runner、shared)实现了清晰的职责分离,并支持跨平台执行。
  2. 环境层级体系赋予每个环境独立的模块图、插件容器和优化器,基于 Proxy 的配置合并则有效避免了重复配置。
  3. Rolldown 统一了工具链——以单一的 Rust 构建器取代了原有的 esbuild + Rollup 组合,同时驱动开发时的转换与生产构建。
  4. 插件系统是核心扩展点——28+ 个核心插件通过 Rolldown 兼容的钩子协同处理模块解析、CSS、静态资源、HTML、导入分析等各项任务。
  5. HMR 与预打包深度集成——模块图、文件监听器与 WebSocket 通道紧密协作,实现亚秒级热更新。

无论你是在开发 Vite 插件、为 Vite 核心贡献代码,还是在调试某个棘手的转换问题,希望这份代码库地图能帮助你找到方向。