Vite 如何解析配置并构建插件流水线
前置知识
- ›第 1 篇:架构概览与代码库导航
- ›了解 Rollup/Rolldown 插件钩子模型(resolveId、load、transform)
- ›TypeScript 泛型与工具类型
Vite 如何解析配置并构建插件流水线
Vite 的配置系统是整个代码库中最复杂的部分,这背后有其充分的理由。它需要将多种来源的配置协调统一:用户的 vite.config.ts(可能是函数、Promise 或普通对象)、来自编程调用方的内联配置、各环境的覆盖选项、向后兼容的 SSR 选项,以及插件对配置的修改——最终将所有这些冻结为一个 ResolvedConfig,供系统其余部分以不可变对象的方式使用。这一切都发生在 src/node/config.ts 中,这个文件超过 2,700 行。
配置文件的加载策略
在解析配置之前,Vite 需要先找到并加载用户的配置文件。loadConfigFromFile() 函数支持三种加载策略,由 configLoader 选项控制:
bundle(默认)——使用 Rolldown 将vite.config.ts编译为临时 JS 文件后再导入。这是最健壮的方式,能透明地处理 TypeScript、路径别名以及来自node_modules的导入。runner(实验性)——使用 Vite 自带的 module runner 在运行时处理配置文件,无需生成临时文件。native(实验性)——依赖 Node.js 原生 ESM 加载器,适用于配置文件本身已是纯 JavaScript 或使用了 Node.js 内置 TypeScript 支持的场景。
CLI 通过 --configLoader 暴露此选项:
// From cli.ts, line 182-183
.option('--configLoader <loader>',
`[string] use 'bundle' to bundle the config with Rolldown, ...`)
配置文件的查找使用 DEFAULT_CONFIG_FILES 列表,包括 vite.config.js、vite.config.mjs、vite.config.ts、vite.config.cjs、vite.config.mts 和 vite.config.cts,按顺序匹配,第一个命中的文件生效。
flowchart TD
A["loadConfigFromFile()"] --> B{"configFile provided?"}
B -->|yes| C["Use specified file"]
B -->|no| D["Search DEFAULT_CONFIG_FILES"]
C --> E{"configLoader?"}
D --> E
E -->|bundle| F["Rolldown-compile to temp .js"]
E -->|runner| G["Vite module runner eval"]
E -->|native| H["Node.js native import()"]
F --> I["import(tempFile)"]
G --> I
H --> I
I --> J{"Export is function?"}
J -->|yes| K["Call with ConfigEnv"]
J -->|no| L["Use object directly"]
K --> M["Return { path, config, dependencies }"]
L --> M
defineConfig() 是一个仅用于类型推断的恒等函数——它原样返回传入的参数,但提供了 TypeScript 的自动补全支持。它兼容所有导出形式:普通对象、Promise 或函数:
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
resolveConfig() 流水线
配置系统的核心是 resolveConfig(),它通过一条多步骤流水线,将用户输入转换为完全解析、已冻结的配置对象。
flowchart TD
A["resolveConfig(inlineConfig, command)"] --> B["Setup Rollup compat shims"]
B --> C["loadConfigFromFile()"]
C --> D["mergeConfig(fileConfig, inlineConfig)"]
D --> E["Filter plugins by 'apply' field"]
E --> F["Sort into pre/normal/post"]
F --> G["Run plugin 'config' hooks"]
G --> H["Ensure client & ssr environments"]
H --> I["Resolve sub-options:<br/>server, build, CSS, SSR, preview"]
I --> J["Resolve per-environment options"]
J --> K["Run 'configEnvironment' hooks"]
K --> L["Resolve plugins via resolvePlugins()"]
L --> M["Run 'configResolved' hooks"]
M --> N["Return frozen ResolvedConfig"]
让我们逐步梳理关键步骤。函数从第 1356 行开始:
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development',
defaultNodeEnv = 'development',
isPreview = false,
patchConfig, patchPlugins,
): Promise<ResolvedConfig> {
patchConfig 和 patchPlugins 是内部钩子,供 createBuilder() 在多环境构建时按环境覆盖 config.build 使用。
加载文件配置后,插件会根据 apply 属性进行过滤(第 1418–1428 行):
const filterPlugin = (p: Plugin | FalsyPlugin): p is Plugin => {
if (!p) return false
if (!p.apply) return true
if (typeof p.apply === 'function') {
return p.apply({ ...config, mode }, configEnv)
}
return p.apply === command
}
这就是为什么你可以给插件设置 apply: 'build' 来在开发模式下跳过它,或者使用函数实现更复杂的条件逻辑。
接下来是第 1443–1460 行的关键步骤:Vite 确保 client 和 ssr 环境始终存在于配置中,并强制约束它们的顺序:
config.environments ??= {}
if (!config.environments.ssr && (!isBuild || config.ssr || config.build?.ssr)) {
config.environments = { ssr: {}, ...config.environments }
}
if (!config.environments.client) {
config.environments = { client: {}, ...config.environments }
}
这种"解构重排"的写法确保 client 排在最前,其次是 ssr,再之后是其他自定义环境——系统其余部分依赖这一固定顺序。
UserConfig → ResolvedConfig 的类型转换
理解类型系统有助于看清配置解析的契约。UserConfig 是用户编写的配置——几乎所有字段都是可选的:
export interface UserConfig extends DefaultEnvironmentOptions {
root?: string
base?: string
publicDir?: string | false
cacheDir?: string
mode?: string
plugins?: PluginOption[]
css?: CSSOptions
server?: ServerOptions
environments?: Record<string, EnvironmentOptions>
// ... 30+ more optional fields
}
ResolvedConfig 是系统内部消费的配置——字段均为必填、只读,且已完整解析:
export interface ResolvedConfig extends Readonly<
Omit<UserConfig, 'plugins' | 'css' | 'json' | ...> & {
root: string // was optional, now required
base: string // resolved to absolute
plugins: readonly Plugin[] // flattened and sorted
css: ResolvedCSSOptions // all defaults filled
environments: Record<string, ResolvedEnvironmentOptions>
// ...
} & PluginHookUtils
> {}
classDiagram
class UserConfig {
root?: string
base?: string
plugins?: PluginOption[]
environments?: Record~string, EnvironmentOptions~
server?: ServerOptions
build?: BuildEnvironmentOptions
}
class ResolvedConfig {
root: string
base: string
plugins: readonly Plugin[]
environments: Record~string, ResolvedEnvironmentOptions~
server: ResolvedServerOptions
build: ResolvedBuildOptions
+getSortedPlugins()
+getSortedPluginHooks()
}
UserConfig ..> ResolvedConfig : resolveConfig()
PluginHookUtils mixin 提供了 getSortedPlugins() 和 getSortedPluginHooks() 方法,它们会缓存按钩子排序后的插件列表,在请求处理阶段实现高效访问。
环境选项的解析
每个环境的选项通过 resolveEnvironmentOptions() 解析,该函数将全局默认值与环境特定的覆盖配置合并。函数根据环境名称判断 consumer 类型(client 或 server):
const consumer = options.consumer ?? (isClientEnvironment ? 'client' : 'server')
开发模式专用的选项由 resolveDevEnvironmentOptions() 处理,它会根据 consumer 类型选取不同的默认值:client 环境使用 preTransformRequests: true 和 recoverable: true,server 环境则使用 moduleRunnerTransform: true。
如第 1 篇所述,PartialEnvironment 构造函数的 Proxy 模式让这一解析过程在运行时透明呈现。当插件读取 this.environment.config.build.outDir 时,Proxy 检测到 build 存在于环境选项中,便返回环境特定的值;而当读取 this.environment.config.root 时,由于该字段不在 ResolvedEnvironmentOptions 中,Proxy 会向上透传到顶层配置。
提示:
configDefaults冻结对象记录了 Vite 使用的所有默认值,是"当用户未指定某项配置时会发生什么"的唯一权威来源。
内部插件的排序
配置解析完成后,resolvePlugins() 负责组装完整的插件数组。其排序是精确且有意为之的:
flowchart TD
subgraph "Pre-user plugins"
A1[optimizedDeps]
A2[watchPackageData]
A3[preAlias]
A4[alias - native or JS]
end
subgraph "User pre plugins"
B[enforce: 'pre' plugins]
end
subgraph "Core plugins"
C1[modulePreloadPolyfill]
C2[oxcResolve]
C3[htmlInlineProxy]
C4[css]
C5[oxc - TS/JSX transform]
C6[json - native]
C7[wasm / webWorker / asset]
end
subgraph "User normal plugins"
D[normal plugins]
end
subgraph "Post-core plugins"
E1[wasmFallback]
E2[define]
E3[cssPost]
E4[buildHtml]
E5[workerImportMetaUrl]
E6[dynamicImportVars / importGlob]
end
subgraph "User post plugins"
F[enforce: 'post' plugins]
end
subgraph "Build post plugins"
G1[importAnalysisBuild / terser]
G2[license / manifest / reporter]
end
subgraph "Dev-only tail"
H1[clientInjections]
H2[cssAnalysis]
H3[importAnalysis]
end
A1 --> B --> C1 --> D --> E1 --> F --> G1 --> H1
注意末尾追加的三个仅限开发模式的插件(第 126–132 行):clientInjectionsPlugin、cssAnalysisPlugin 和 importAnalysisPlugin。这是开发模式下最重要的三个插件——其中 importAnalysisPlugin 负责将源码中的每一个 import 重写为无打包 ESM 的形式——它们必须排在最后,以便处理所有其他 transform 的输出结果。
除了全局排序外,每个插件内部的钩子还可以指定 order: 'pre' | 'post'。getSortedPluginsByHook() 函数通过高效的原地插入方式处理这种逐钩子的排序。
Vite 专有的插件钩子
Vite 的 Plugin 接口 在 Rolldown 的 RolldownPlugin 基础上扩展了多个 Rollup/Rolldown 中没有对应项的钩子:
| 钩子 | 触发时机 | 用途 |
|---|---|---|
config |
解析前 | 修改或扩展原始用户配置 |
configEnvironment |
每个环境 | 修改环境特定配置 |
configResolved |
解析后 | 读取最终冻结后的配置 |
configureServer |
服务器创建时 | 添加 middleware、保存服务器引用 |
configurePreviewServer |
预览服务器创建时 | 同上,用于预览服务器 |
transformIndexHtml |
HTML 响应时 | 注入标签、转换 HTML 内容 |
hotUpdate |
文件变更时 | 控制 HMR 更新的传播 |
buildApp |
构建开始时 | 编排多环境构建流程 |
enforce 属性(第 202–214 行)控制用户插件相对于 Vite 内部插件的插入位置:
enforce?: 'pre' | 'post'
// Plugin invocation order:
// - alias resolution
// - `enforce: 'pre'` plugins
// - vite core plugins
// - normal plugins
// - vite build plugins
// - `enforce: 'post'` plugins
// - vite build post plugins
applyToEnvironment 钩子(第 227–229 行)是 Environment API 新增的能力——它允许插件按环境条件激活,甚至为不同环境返回完全不同的插件实例:
applyToEnvironment?: (
environment: PartialEnvironment,
) => boolean | Promise<boolean> | PluginOption
提示:
sharedDuringBuild标志(第 184 行)控制在执行vite build --app时,插件实例是否跨环境共享。默认情况下,为了向后兼容,每个环境都会重新创建插件实例。将其设置为true可以启用更高效的共享模式。
下一步
理解了配置解析流水线和插件排序机制后,接下来我们将探索浏览器向开发服务器请求模块时究竟发生了什么。下一篇文章将完整追踪一次请求的生命周期:从 connect middleware 栈,经过 transform 流水线,穿越 plugin container,最终进入跟踪所有模块关系、支撑 HMR 运作的模块图。