Read OSS

生产构建、Builder API 与 Vite 的 SSR 模块运行器

高级

前置知识

  • 第 1-4 篇:全面理解架构、配置、开发服务器、HMR 与模块图
  • Rollup/Rolldown 打包概念(输入选项、输出选项、chunk、代码分割)
  • SSR 概念(服务端渲染、模块求值)

生产构建、Builder API 与 Vite 的 SSR 模块运行器

Vite 的开发服务器以未打包的 ESM 形式提供模块,而生产环境则需要经过打包、压缩和代码分割的产物。Vite 8 将这一工作交给了 Rolldown —— 加上 Environment API,如今一次 vite build 就能在统一的流水线中为多个目标(客户端、SSR、边缘 Worker)分别生成产物。本文将介绍构建流水线、SSR 模块运行器,以及正在模糊开发与生产边界的实验性全量打包开发模式。

Builder API 与多环境协调

运行 vite build 时,第 343-382 行的 CLI 处理程序会创建一个 builder 并调用 buildApp()

const { createBuilder } = await import('./build')
const builder = await createBuilder(inlineConfig, null)
await builder.buildApp()

createBuilder() 函数解析配置后,为 config.environments 中的每个条目创建一个 BuildEnvironment

flowchart TD
    A["createBuilder(inlineConfig)"] --> B["resolveConfigToBuild()"]
    B --> C{"builder config provided?"}
    C -->|"yes (--app)"| D["Create BuildEnvironment per environment"]
    C -->|"no (legacy)"| E["Create single BuildEnvironment<br/>(client or ssr)"]
    D --> F["ViteBuilder { environments, build, buildApp }"]
    E --> F
    F --> G["builder.buildApp()"]
    G --> H["Run 'buildApp' plugin hooks (pre + normal)"]
    H --> I["config.builder.buildApp(builder)"]
    I --> J["Run 'buildApp' plugin hooks (post)"]
    J --> K{"Any environment built?"}
    K -->|yes| L["Done"]
    K -->|no| M["Fallback: build all environments sequentially"]

buildApp() 方法采用了一套有趣的协调模式:plugin 的 buildApp hook 按顺序执行,用户的 config.builder.buildApp 回调则被插入到普通 hook 和 post hook 之间。如果所有 hook 执行完毕后仍没有任何环境被构建,兜底逻辑会接管并按顺序构建所有环境。

多环境构建中,plugin 实例的管理是一个关键的设计考量。默认情况下,每个环境都会重新解析配置并获得全新的 plugin 实例(第 1860-1914 行):

if (!configBuilder.sharedConfigBuild) {
  environmentConfig = await resolveConfigToBuild(
    inlineConfig, patchConfig, patchPlugins,
  )
}

sharedPlugins 选项和每个 plugin 的 sharedDuringBuild 标志可以让 plugin 实例在多个环境之间共享。patchPlugins 函数负责将新创建的 plugin 实例替换为首次解析配置时生成的共享实例。

BuildEnvironment 与 Rolldown 集成

DevEnvironment 相比,BuildEnvironment 有意保持轻量 —— 没有模块图、没有 plugin 容器、也没有依赖优化器:

export class BuildEnvironment extends BaseEnvironment {
  mode = 'build' as const
  isBuilt = false

  constructor(name, config, setup?) {
    // merge environment options, call super
  }

  async init(): Promise<void> {
    if (this._initiated) return
    this._initiated = true
  }
}

实际的打包工作由 buildEnvironment() 完成,它调用 resolveRolldownOptions() 将 Vite 的配置映射为 Rolldown 的输入/输出格式:

sequenceDiagram
    participant B as builder.build(env)
    participant BE as buildEnvironment()
    participant RO as resolveRolldownOptions()
    participant RD as Rolldown
    participant P as Plugins

    B->>BE: buildEnvironment(environment)
    BE->>RO: resolveRolldownOptions(environment)
    Note over RO: Map config.build → Rolldown input<br/>Resolve entry points<br/>Configure output format<br/>Inject environment into plugins
    RO-->>BE: RolldownOptions
    BE->>RD: rolldown(options)
    RD-->>BE: bundle
    BE->>RD: bundle.write() or bundle.generate()
    RD->>P: generateBundle / writeBundle hooks
    RD-->>BE: RolldownOutput

resolveRolldownOptions() 函数根据配置确定入口点:客户端构建使用 index.html,库模式使用显式指定的入口,rollupOptions.input 则作为覆盖选项。它还会用 injectEnvironmentToHooks() 包装每个 plugin,确保在所有 plugin hook 中都能访问到 this.environment

提示: ViteBuilder 接口暴露了 build(environment) 方法,可以单独构建某个环境。框架集成可以通过 buildApp hook 控制构建顺序 —— 例如先构建客户端以收集资产 manifest,再将其传给 SSR 构建。

构建专用 Plugin

resolveBuildPlugins() 函数负责组装仅在生产构建期间运行的 plugin:

flowchart LR
    subgraph "pre plugins"
        A1[prepareOutDir]
        A2[rollup-options-plugins]
        A3[webWorkerPost]
    end
    subgraph "post plugins"
        B1[importAnalysisBuild]
        B2[esbuild minifier]
        B3[terser minifier]
        B4[license extraction]
        B5[manifest generation]
        B6[ssrManifest]
        B7[buildReporter]
        B8[loadFallback - native]
    end

buildImportAnalysisPlugin 是第三篇中介绍的仅限开发环境使用的 importAnalysisPlugin 的构建时对应版本。开发版 plugin 将 import 重写为未打包的 ESM,而构建版 plugin 负责处理模块预加载 —— 注入 <link rel="modulepreload"> 提示和 __vitePreload() 包装,防止动态 import 产生瀑布式请求。

post plugin 末尾的原生 Rolldown plugin nativeLoadFallbackPlugin() 值得关注 —— 它为所有未被其他 plugin 处理的模块提供文件系统加载能力,是构建流程的最后一道保障。

构建模式下的 CSS 处理

CSS 处理使用三个 plugin 组成的流水线,其行为在开发与构建模式下有所不同:

flowchart TD
    subgraph "cssPlugin (transform)"
        A1["Preprocessor: Sass/Less/Stylus"] --> A2["PostCSS transforms"]
        A2 --> A3["CSS Modules scoping"]
        A3 --> A4["URL rewriting"]
    end
    subgraph "cssPostPlugin (renderChunk)"
        B1["Extract CSS from JS chunks"]
        B1 --> B2["Generate .css asset files"]
        B2 --> B3["Minify with LightningCSS"]
    end
    subgraph "cssAnalysisPlugin (dev only)"
        C1["Track CSS dependencies"]
        C1 --> C2["HMR support for CSS"]
    end
    A4 --> B1

cssPlugin 负责转换阶段,包括运行预处理器、PostCSS、CSS Modules 作用域处理和 URL 重写。在构建阶段,cssPostPlugin 负责提取 chunk 中的 CSS:当 build.cssCodeSplittrue(默认值)时,每个 JS chunk 都会生成对应的 CSS 文件;为 false 时,所有 CSS 会合并为单个文件。

Vite 8 中,CSS 压缩在服务端环境默认使用 LightningCSS,客户端环境则遵循 build.minify 的设置。build.cssMinify 选项可以进行显式控制。

模块运行器:SSR 模块执行

ModuleRunner 是 Vite 在开发阶段执行服务端模块的解决方案。它从 DevEnvironment 获取经过转换的代码并执行,同时维护一个模块缓存 —— 全程支持 HMR。

sequenceDiagram
    participant App as SSR Application
    participant MR as ModuleRunner
    participant T as Transport
    participant DE as DevEnvironment
    participant PC as pluginContainer

    App->>MR: runner.import('/src/server.ts')
    MR->>T: fetchModule('/src/server.ts')
    T->>DE: environment.fetchModule(id)
    DE->>PC: transformRequest(url)
    PC-->>DE: TransformResult { code, map }
    DE-->>T: FetchResult { code }
    T-->>MR: FetchResult
    MR->>MR: ESModulesEvaluator.runExternalModule(code)
    Note over MR: new AsyncFunction(ssrImport, ssrExport, ...)(code)
    MR-->>App: module.exports

第 53-80 行的构造函数负责初始化 transport,并可选地创建 HMRClient

constructor(
  public options: ModuleRunnerOptions,
  public evaluator: ModuleEvaluator = new ESModulesEvaluator(),
) {
  this.transport = normalizeModuleRunnerTransport(options.transport)
  if (options.hmr !== false) {
    this.hmrClient = new HMRClient(
      resolvedHmrLogger,
      this.transport,
      ({ acceptedPath }) => this.import(acceptedPath),
    )
    this.transport.connect(createHMRHandlerForRunner(this))
  }
}

ESModulesEvaluator 将转换后的代码转为一个函数,使用 Vite SSR 模块格式 —— 以 __vite_ssr_import____vite_ssr_export____vite_ssr_import_meta__ 作为互操作机制。

服务端的 fetchModule() 函数充当运行器请求与环境转换流水线之间的桥梁。它首先检查模块是否为 Node.js 内置模块或外部 URL,若是则返回 externalize 结果;否则调用 environment.transformRequest(url) 并返回转换后的代码。

可运行环境与可获取环境

Vite 提供了两种预置的 SSR 环境工厂,分别代表两种不同的执行模型:

RunnableDevEnvironment 在与 Vite 服务器相同的 Node.js 进程中运行模块。它使用 createServerHotChannel() 实现进程内的 HMR 通信,无需 WebSocket:

export function createRunnableDevEnvironment(
  name, config, context = {},
): RunnableDevEnvironment {
  if (context.transport == null) {
    context.transport = createServerHotChannel()
  }
  return new RunnableDevEnvironment(name, config, context)
}

FetchableDevEnvironment 面向远程运行时(边缘 Worker、Cloudflare Workers),这类环境无法在进程内运行模块。它需要一个 handleRequest 函数,并通过 Fetch API 进行通信:

export function createFetchableDevEnvironment(
  name, config, context,
): FetchableDevEnvironment {
  if (!context.handleRequest) {
    throw new TypeError(
      'FetchableDevEnvironment requires a `handleRequest` method'
    )
  }
  return new FetchableDevEnvironment(name, config, context)
}
flowchart TD
    subgraph "In-process SSR"
        A[RunnableDevEnvironment] --> B[createServerHotChannel]
        B --> C[ModuleRunner<br/>same Node.js process]
    end
    subgraph "Remote SSR"
        D[FetchableDevEnvironment] --> E[Custom Transport]
        E --> F[ModuleRunner<br/>edge/worker runtime]
    end
    subgraph "Shared"
        G[DevEnvironment base class]
    end
    G --> A
    G --> D

提示: 框架作者应使用 isRunnableDevEnvironment()isFetchableDevEnvironment() 类型守卫在运行时判断环境类型,而不是直接检查类名。

实验性全量打包开发模式

FullBundleDevEnvironment 是未打包开发服务的实验性替代方案。通过 --experimentalBundleexperimental.bundledDev: true 启用后,它会在开发阶段使用 Rolldown 的 dev() API 对应用进行打包,并将产物保存在内存中供浏览器访问:

import { dev } from 'rolldown/experimental'

export class FullBundleDevEnvironment extends DevEnvironment {
  private devEngine!: DevEngine
  memoryFiles: MemoryFiles = new MemoryFiles()
  // ...
}

MemoryFiles 类用 Map<string, MemoryFile> 存储打包产物,memoryFilesMiddleware 负责将这些文件提供给浏览器。DevEngine 通过 Rolldown 自身的变更检测机制实现 HMR,完全绕过了 Vite 的模块图。

这一模式代表了 Vite 未来可能的演进方向 —— 当开发和生产都使用同一个打包工具时,两者之间的差距将大幅缩小。各方案的权衡一目了然:

未打包开发 打包开发 生产构建
启动速度 快(无需打包) 较慢(初始打包) 最慢(完整优化)
HMR 模块级粒度 Bundle 级(via Rolldown) N/A
开发/生产一致性 存在差异 高度一致
状态 稳定 实验性 稳定

打包开发模式目前仍有局限 —— handleHotUpdate/hotUpdate plugin hook 尚未完全支持,模块图集成也不完整。但它清晰地展示了架构的发展方向:随着 Rolldown 足够快到可以用于开发,独立的开发/构建代码路径最终有望合二为一。

系列总结

通过这六篇文章,我们从 CLI 入口点出发,依次走过了 Vite 8 的配置解析、开发服务器的中间件栈与转换流水线、HMR 与依赖预打包,最终抵达生产构建与 SSR 模块执行。以下是核心架构洞见:

  1. Environment API 是最基础的抽象层 —— 每个子系统(模块图、plugin 容器、依赖优化器、热更新通道)都是按环境独立存在的
  2. PartialEnvironment 中基于 Proxy 的配置合并让环境专属配置对 plugin 完全透明
  3. Plugin 容器在开发阶段模拟 Rollup 的执行模型,使同一套 plugin 能同时工作于开发和构建
  4. Rolldown 统一消除了 esbuild/Rollup 的分裂局面,在依赖优化和生产打包中提供一致的行为
  5. src/shared/ 中的共享 HMR 协议确保浏览器与 SSR 运行器具有完全相同的 HMR 语义

整个代码库在复杂度之下仍保持了清晰的结构 —— 理解这六个层次,你就掌握了在其中任意角落自如导航的能力。