配置解析与多环境系统
前置知识
- ›第 1 篇:架构概览与代码库导航
- ›了解 JavaScript Proxy 对象
- ›熟悉 Rollup/Rolldown plugin hook 的概念
配置解析与多环境系统
Vite 的配置系统远不止是解析一个 vite.config.ts 文件那么简单。它需要跨 client、SSR 以及框架自定义的多个环境,完成配置的解析、合并、校验与冻结。这整个过程由一条精确的 10 步流水线驱动,用户插件、环境钩子与向后兼容层在其中交织运行。
理解这条流水线至关重要,因为 Vite 中几乎所有其他子系统都依赖它产出的 ResolvedConfig 对象。如果你曾好奇为何 environment.config.build 与顶层 config.build 有时会不同,或者 esbuild 的某个选项是如何被悄悄转换为 Rolldown 等价项的,本文将一一解答。
resolveConfig 流水线
resolveConfig 函数是所有配置解析的统一入口。它接收一个 InlineConfig、一个命令('serve' 或 'build')以及可选的内部参数。下面是这条流水线的整体示意:
flowchart TD
A["1. Setup Rollup compat shims"] --> B["2. Load config file"]
B --> C["3. Merge file config with inline config"]
C --> D["4. Filter plugins by 'apply' field"]
D --> E["5. Sort plugins: pre / normal / post"]
E --> F["6. Run plugin 'config' hooks"]
F --> G["7. Ensure default environments (client, ssr)"]
G --> H["8. Resolve sub-configs<br/>(server, build, CSS, etc.)"]
H --> I["9. Run 'configResolved' hooks"]
I --> J["10. Return frozen ResolvedConfig"]
第 1 步会立即对 build、worker 和 optimizeDeps 配置调用 setupRollupOptionCompat——这是 Rollup 到 Rolldown 迁移的第一道防线,负责将旧有的 rollupOptions 字段映射到对应的 Rolldown 等价项。
第 4–5 步完成插件的排序。首先根据 apply 字段过滤插件(仅保留与当前命令匹配的插件),再按 enforce 拆分为三组:
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawPlugins)
第 6 步依次执行每个插件的 config 钩子,让插件有机会在配置解析前对用户配置进行修改。@vitejs/plugin-react 等插件正是在这里注入其 Rolldown transform 选项的。
第 7 步对环境系统而言至关重要。在第 1447–1462 行,Vite 确保 client 和 ssr 环境始终存在:
if (!config.environments.client) {
config.environments = { client: {}, ...config.environments }
}
展开顺序很关键:client 被放在最前面,确保 Object.keys() 迭代时优先处理它,然后才是自定义环境。
环境类层级体系
Vite 8 引入了一套环境类层级体系,每个环境都拥有独立的 module graph、plugin container、依赖优化器和 hot channel:
classDiagram
class PartialEnvironment {
+name: string
+config: ResolvedConfig & ResolvedEnvironmentOptions
+logger: Logger
-_topLevelConfig: ResolvedConfig
-_options: ResolvedEnvironmentOptions
+getTopLevelConfig(): ResolvedConfig
}
class BaseEnvironment {
+plugins: readonly Plugin[]
-_initiated: boolean
}
class UnknownEnvironment {
+mode: "unknown"
}
class DevEnvironment {
+mode: "dev"
+moduleGraph: EnvironmentModuleGraph
+depsOptimizer?: DepsOptimizer
+hot: NormalizedHotChannel
+pluginContainer: EnvironmentPluginContainer
}
class BuildEnvironment {
+mode: "build"
+isBuilt: boolean
}
class ScanEnvironment {
+mode: "scan"
+pluginContainer: EnvironmentPluginContainer
}
PartialEnvironment <|-- BaseEnvironment
BaseEnvironment <|-- UnknownEnvironment
BaseEnvironment <|-- DevEnvironment
BaseEnvironment <|-- BuildEnvironment
BaseEnvironment <|-- ScanEnvironment
PartialEnvironment 是整个层级的根节点,存储环境名称、顶层配置的引用、环境专属选项,以及基于 Proxy 的合并配置(稍后详述)。它还会创建一个自定义 logger,以带颜色编码的格式为日志信息添加环境名前缀。
BaseEnvironment 在此基础上增加了插件访问能力和初始化标志位。
DevEnvironment 是开发阶段的核心。它掌管 module graph、plugin container、依赖优化器、待处理请求的追踪以及 hot channel,每个环境都完全自包含。
联合类型定义于 environment.ts:
export type Environment =
| DevEnvironment
| BuildEnvironment
| ScanEnvironment
| UnknownEnvironment
提示: 在编写插件代码时,通过检查
this.environment.mode来判断当前可用的能力。只有DevEnvironment才拥有moduleGraph和depsOptimizer。不要用mode !== 'build'来推断是否处于开发模式——请明确使用mode === 'dev'。UnknownEnvironment类的存在,正是为了捕获这种反模式。
基于 Proxy 的环境配置合并
Vite 代码库中最精妙的设计之一,是环境配置与顶层配置的合并方式。每个 PartialEnvironment 的 config 属性本质上是一个 JavaScript Proxy:
sequenceDiagram
participant Plugin
participant Proxy (environment.config)
participant EnvOptions
participant TopLevelConfig
Plugin->>Proxy (environment.config): Read config.resolve.alias
Proxy (environment.config)->>EnvOptions: Has 'resolve'?
alt Property exists in environment options
EnvOptions-->>Proxy (environment.config): Return env-specific value
else Property only in top-level
Proxy (environment.config)->>TopLevelConfig: Return top-level value
end
Proxy (environment.config)-->>Plugin: Resolved value
以下是 baseEnvironment.ts 中的实际实现:
this.config = new Proxy(
options as ResolvedConfig & ResolvedEnvironmentOptions,
{
get: (target, prop: keyof ResolvedConfig) => {
if (prop === 'logger') {
return this.logger
}
if (prop in target) {
return this._options[prop as keyof ResolvedEnvironmentOptions]
}
return this._topLevelConfig[prop]
},
},
)
逻辑简洁明了:如果属性存在于环境专属选项中,则使用该值;否则回退到顶层配置。这样一来,插件代码可以直接写 this.environment.config.build.outDir,而无需关心 outDir 是按环境配置的还是全局配置的——Proxy 会透明地处理回退逻辑。
这种方案也规避了另一种做法的弊端——为每个环境深拷贝并合并整份配置,那样既浪费资源,又容易出错。
esbuild 到 Rolldown 的兼容层
Vite 8 内置了一套完整的兼容层,用于将遗留的 optimizeDeps.esbuildOptions 映射到 Rolldown 等价项。这部分逻辑位于配置解析流水线的第 1190–1302 行。
转换过程井然有序,逐一检查并映射每个 esbuild 选项:
| esbuild 选项 | Rolldown 等价项 | 说明 |
|---|---|---|
minify |
rolldownOptions.output.minify |
直接映射 |
treeShaking |
rolldownOptions.treeshake |
直接映射 |
define |
rolldownOptions.transform.define |
直接映射 |
loader |
rolldownOptions.moduleTypes |
过滤掉 copy、css、default、file、local-css |
preserveSymlinks |
rolldownOptions.resolve.symlinks |
布尔值取反 |
resolveExtensions |
rolldownOptions.resolve.extensions |
直接映射 |
mainFields |
rolldownOptions.resolve.mainFields |
直接映射 |
conditions |
rolldownOptions.resolve.conditionNames |
直接映射 |
keepNames |
rolldownOptions.output.keepNames |
直接映射 |
platform |
rolldownOptions.platform |
直接映射 |
值得关注的是,代码对无法转换的选项也做了细致的注释说明。第 1267–1301 行将不可转换的选项分为三类:从根本上无法映射的、理论上可映射但不值得处理的,以及本身不适合转换的。
当检测到 esbuild 选项时,这一兼容层会发出警告,引导用户改用 optimizeDeps.rolldownOptions。它是一座务实的桥梁——依赖 esbuild 选项的生态插件在迁移完成之前仍可正常运行。
perEnvironmentState 工具函数
对于需要管理每个环境独立状态的插件作者,Vite 在 environment.ts 中提供了一个实用工具:
export function perEnvironmentState<State>(
initial: (environment: Environment) => State,
): (context: PluginContext) => State {
const stateMap = new WeakMap<Environment, State>()
return function (context: PluginContext) {
const { environment } = context
let state = stateMap.get(environment)
if (!state) {
state = initial(environment)
stateMap.set(environment, state)
}
return state
}
}
它基于 WeakMap 实现了一个懒加载的状态访问器,按环境隔离状态。插件钩子调用 getState(this) 即可获取当前环境专属的状态对象。使用 WeakMap 还能确保在环境销毁后,对应的状态会被自动垃圾回收。
下一篇预告
至此,我们已经了解了 Vite 如何将用户配置解析为一个冻结的 ResolvedConfig,并通过 Proxy 透明地代理每个环境的专属选项。下一篇文章将进一步探讨这份配置如何投入实际运作:我们将追踪 createServer 如何组装 HTTP server、按精确顺序注册 18 层 middleware,以及 transform 流水线如何将一个针对 .ts 文件的 HTTP 请求,最终转化为完整解析、import 已重写的 JavaScript。