生产构建、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)方法,可以单独构建某个环境。框架集成可以通过buildApphook 控制构建顺序 —— 例如先构建客户端以收集资产 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.cssCodeSplit 为 true(默认值)时,每个 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 是未打包开发服务的实验性替代方案。通过 --experimentalBundle 或 experimental.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 模块执行。以下是核心架构洞见:
- Environment API 是最基础的抽象层 —— 每个子系统(模块图、plugin 容器、依赖优化器、热更新通道)都是按环境独立存在的
PartialEnvironment中基于 Proxy 的配置合并让环境专属配置对 plugin 完全透明- Plugin 容器在开发阶段模拟 Rollup 的执行模型,使同一套 plugin 能同时工作于开发和构建
- Rolldown 统一消除了 esbuild/Rollup 的分裂局面,在依赖优化和生产打包中提供一致的行为
src/shared/中的共享 HMR 协议确保浏览器与 SSR 运行器具有完全相同的 HMR 语义
整个代码库在复杂度之下仍保持了清晰的结构 —— 理解这六个层次,你就掌握了在其中任意角落自如导航的能力。