《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与多环境构建的架构 - 掌握
createBuilder和buildApp的编排机制 - 了解构建 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 的源码中,我们可以清晰地看到这一迁移的痕迹:
// 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 的别名,使用旧配置名的项目不需要做任何修改就能正常工作:
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() 函数出人意料地简洁。它的全部工作就是创建一个构建器实例,然后委托构建器对第一个环境执行构建。这种简洁性不是偷工减料,而是优秀的抽象设计——真正的复杂性被封装在了 createBuilder 和 buildEnvironment 中:
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 函数的构建模式封装。两个可选的回调参数——patchConfig 和 patchPlugins——是多环境构建的关键支撑,它们允许在配置解析完成后对结果进行环境特定的修补:
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 构建行为的基础:
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'<br/>Chrome 111+, Firefox 114+, Safari 16.4+"]
B["minify"] --> B1["client: 'oxc' (Rust 压缩器)<br/>server: false (不压缩)"]
C["outDir"] --> C1["'dist'"]
D["assetsDir"] --> D1["'assets'"]
E["assetsInlineLimit"] --> E1["4096 字节 (4KB)"]
F["sourcemap"] --> F1["false"]
G["cssCodeSplit"] --> G1["非 lib 模式: true<br/>lib 模式: false"]
H["emitAssets"] --> H1["client: true (输出资源)<br/>其他环境: false"]
end
构建目标的语义化
baseline-widely-available 是 Vite 引入的一个语义化目标值。它代表 "在 2026-01-01 之前被广泛支持的浏览器基线",会被展开为具体的浏览器版本列表。这种语义化的目标定义比直接指定 ['chrome111', 'firefox114', 'safari16.4'] 更加直观和可维护。去重处理是从 Rollup 到 Rolldown 迁移带来的需求——esbuild 允许目标列表中的重复项,但 oxc 压缩器不允许。
13.4 Rolldown 选项构建
resolveRolldownOptions:配置生成的核心
这是整个构建流程中最关键的配置生成函数。它将 Vite 的高层抽象配置翻译为 Rolldown 可以直接消费的底层选项。每一个配置项的选择都蕴含着对不同构建场景的深入考量:
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 的输出阶段生成配置。文件命名策略是这里最重要的决策——它直接影响产物的缓存行为和部署方式:
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<br/>(无 hash,无 assets 目录)"]
B["库模式"] --> B1["入口: resolveLibFilename() 计算<br/>分块: [name]-[hash].js<br/>资源: [name].[ext](无 hash)"]
C["应用模式"] --> C1["入口: assets/[name]-[hash].js<br/>分块: assets/[name]-[hash].js<br/>资源: 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.json 的 type 字段共同决定文件的模块格式。Vite 的扩展名解析逻辑确保了输出文件在 Node.js 中能被正确识别。当 package.json 声明 "type": "module" 时,.js 文件被视为 ES Module,CJS 格式需要使用 .cjs 扩展名;反之,.js 被视为 CommonJS,ES Module 需要使用 .mjs。
13.5 构建执行引擎
buildEnvironment:核心打包逻辑
这是实际执行 Rolldown 打包的函数。它的结构清晰地反映了构建的三个阶段:准备(配置解析)、执行(打包)、收尾(元数据注入和资源清理)。错误处理包裹了整个流程,确保即使构建失败,bundle 资源也会被正确释放:
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 层执行<br/>模块解析<br/>依赖图构建<br/>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 阶段——元数据最常被访问的时候——最终文件名可能还未确定:
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 可以更精确地控制属性的行为:
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 通过包装每个钩子函数来注入环境上下文。它创建了插件的浅拷贝(保持原型链),然后逐个替换钩子函数为包装版本:
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
}
resolveId、load 和 transform 有专门的包装函数,因为它们除了注入环境之外还需要注入 ssr 标志。其他标准的 Rollup 钩子使用通用的 wrapEnvironmentHook,其中 renderChunk 和 generateBundle 等钩子还有额外的元数据注入逻辑。
SSR 标志的废弃路径
Vite 正在逐步推动生态系统从使用 options.ssr 标志转向使用 this.environment 来判断当前环境。这个迁移通过 getter 陷阱实现了优雅的过渡——当插件访问 options.ssr 时,如果启用了废弃警告,会提示开发者使用新的 API:
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 接口提供了统一的编排机制来管理这些并行的构建任务:
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()<br/>patchConfig: 覆盖 config.build<br/>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 的执行。如果所有钩子执行完毕后没有任何环境被标记为已构建,系统会自动回退到顺序构建所有环境——这是一个优秀的默认行为,确保即使没有自定义的构建编排逻辑,所有环境也能被正确构建:
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<br/>清理和准备输出目录"]
P2["vite:rollup-options-plugins<br/>执行用户的 Rolldown 插件"]
P3["webWorkerPostPlugin<br/>Worker 脚本处理"]
end
subgraph "核心插件(用户配置的插件在此区间)"
M1["vite:resolve (模块解析)"]
M2["vite:html (HTML 转换)"]
M3["vite:css (样式处理)"]
M4["vite:asset (资源处理)"]
M5["vite:define (常量替换)"]
end
subgraph "Post 插件(在用户插件之后)"
O1["buildImportAnalysisPlugin<br/>动态导入分析与 preload"]
O2["terserPlugin<br/>可选的 Terser 压缩"]
O3["licensePlugin<br/>第三方许可证收集"]
O4["manifestPlugin<br/>资源清单生成"]
O5["ssrManifestPlugin<br/>SSR 清单生成"]
O6["buildReporterPlugin<br/>构建报告(大小统计)"]
O7["nativeLoadFallbackPlugin<br/>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 选项:
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.message 后 e.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)两种环境:
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 团队对生态系统现状的务实认知。虽然独立解析意味着多环境构建需要多次解析配置(有一定的性能开销),但它保证了插件的状态隔离——这在当前大多数插件还没有为多环境设计的情况下是必要的。sharedConfigBuild 和 sharedPlugins 选项为确信自己的插件是多环境安全的项目提供了优化路径。
为什么使用 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 的构建引擎是一个精心编排的多层架构,每一层都有清晰的职责和边界:
- 入口层:
build()/createBuilder()提供简洁的公共 API,隐藏内部复杂性 - 配置层:
resolveBuildEnvironmentOptions()/resolveRolldownOptions()将高层语义翻译为底层选项 - 编排层:
ViteBuilder/buildApp()管理多环境构建的执行顺序和资源共享 - 执行层:
buildEnvironment()驱动 Rolldown 完成实际的打包工作 - 元数据层:
ChunkMetadataMap/injectEnvironmentToHooks()在 Rolldown 和 Vite 插件之间搭建信息桥梁
从 Rollup 到 Rolldown 的迁移不仅是一次性能升级,更是 Vite 技术战略的关键落子。Rolldown 作为 Rust 实现的打包器,与 Vite 的其他 Rust 组件形成了统一的工具链。这意味着未来模块解析、代码转换、打包和压缩可以在同一个 Rust 进程中协作完成,避免了 JavaScript 和原生代码之间的频繁切换。
多环境构建架构则体现了 Vite 团队对现代 Web 应用复杂性的深刻理解。一个应用可能需要同时为浏览器、Node.js 服务端、Web Worker 等多个运行时生成优化的产物。统一的 Builder API 在概念上保持简洁——所有环境共享相同的构建管线——在实现上保持灵活——每个环境可以有独立的配置、插件和输出策略。这种简洁与灵活的平衡是优秀架构设计的标志。