Appearance
第16章 Environment API
开篇引言
在 Vite 6 之前,一个 Vite 服务器实例只有一个统一的模块图、插件管线和依赖优化器。当项目需要同时处理客户端代码和 SSR 代码时,这些共享的基础设施不得不通过参数(如 ssr: boolean)来区分行为。这种方式简单但脆弱 -- 当需要支持更多的运行环境(如 RSC、Service Worker、Edge Runtime)时,布尔参数无法扩展。
Vite 6 引入的 Environment API 从根本上改变了这一架构。每个"环境"拥有独立的模块图、插件容器和依赖优化器,它们通过共享的顶层配置保持协调。这不是一次简单的重构,而是 Vite 向"通用构建编排器"角色演进的关键一步。
本章将从 environment.ts、baseEnvironment.ts、server/environment.ts、build.ts、optimizer/scan.ts 等源码文件出发,深入分析 Environment API 的类型体系、生命周期管理和多环境协作机制。
本章要点
- 理解 Environment API 的设计动机与架构目标
- 掌握
PartialEnvironment -> BaseEnvironment -> DevEnvironment/BuildEnvironment/ScanEnvironment的类型层次 - 分析
perEnvironmentPlugin和perEnvironmentState的多环境插件适配模式 - 理解每环境独立的模块图、插件容器和依赖优化器
- 掌握环境配置的 Proxy 合并策略
16.1 设计动机
16.1.1 从 ssr 布尔值到多环境
在 Vite 5 及更早版本中,SSR 支持是通过在 API 中传递 ssr: boolean 参数来实现的:
typescript
// Vite 5 风格
server.transformRequest(url, { ssr: true })
server.moduleGraph.getModuleByUrl(url, true)这种方式存在几个问题:
- 不可扩展:当需要支持第三种环境(如 React Server Components 的 RSC 环境)时,布尔值无法表达
- 共享污染:客户端和 SSR 共享同一个模块图,模块的
transformResult和ssrTransformResult混存在同一个节点上 - 优化冲突:客户端和 SSR 可能需要不同的依赖优化策略,共享的优化器无法同时满足
- 插件歧义:插件需要在运行时检查
ssr参数来决定行为,增加了认知负担
16.1.2 目标架构
Environment API 的目标是将"环境"提升为一等公民:
16.2 类型体系
16.2.1 类继承层次
Environment API 定义了一个精心设计的类继承层次:
16.2.2 PartialEnvironment:配置层
PartialEnvironment 是整个层次的基础,负责环境名称验证、配置合并和日志初始化:
typescript
export class PartialEnvironment {
name: string
config: ResolvedConfig & ResolvedEnvironmentOptions
logger: Logger
constructor(
name: string,
topLevelConfig: ResolvedConfig,
options: ResolvedEnvironmentOptions = topLevelConfig.environments[name],
) {
// 环境名称只允许字母数字和 $ _
if (!/^[\w$]+$/.test(name)) {
throw new Error(
`Invalid environment name "${name}". Environment names must only contain alphanumeric characters and "$", "_".`,
)
}
this.name = name
this._topLevelConfig = topLevelConfig
this._options = options
// 核心设计:通过 Proxy 实现配置合并
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]
},
},
)
// 为每个环境配置带颜色标记的 logger
const environment = colors.dim(`(${this.name})`)
const colorIndex =
[...this.name].reduce((acc, c) => acc + c.charCodeAt(0), 0) %
environmentColors.length
// ...
}
}Proxy 配置合并是 Environment API 最精妙的设计之一。environment.config 是一个 Proxy 对象:
- 当访问的属性存在于环境选项(
_options)中时,返回环境特定的值 - 当访问的属性不存在于环境选项中时,回退到顶层配置(
_topLevelConfig)
这种设计使得环境配置可以选择性覆盖顶层配置,同时共享大部分通用配置,避免了完整配置的拷贝。
日志颜色化:每个环境根据名称的字符编码计算一个颜色索引,使得日志输出中不同环境的信息可以通过颜色直观区分:
typescript
const environmentColors = [
colors.blue, // 蓝
colors.magenta, // 品红
colors.green, // 绿
colors.gray, // 灰
]16.2.3 BaseEnvironment:插件层
BaseEnvironment 在 PartialEnvironment 基础上增加了插件访问能力和初始化状态追踪:
typescript
export class BaseEnvironment extends PartialEnvironment {
get plugins(): readonly Plugin[] {
return this.config.plugins
}
_initiated: boolean = false
constructor(
name: string,
config: ResolvedConfig,
options: ResolvedEnvironmentOptions = config.environments[name],
) {
super(name, config, options)
}
}plugins 属性通过 getter 从配置中读取,由于 config 是 Proxy,这意味着每个环境可以拥有独立的插件列表。_initiated 标志用于确保 init() 方法只被调用一次。
16.2.4 UnknownEnvironment:扩展保护
typescript
export class UnknownEnvironment extends BaseEnvironment {
mode = 'unknown' as const
}UnknownEnvironment 的设计目的体现在源码注释中:
This class discourages users from inversely checking the
modeto determine the type of environment, e.g.jsconst isDev = environment.mode !== 'build' // bad const isDev = environment.mode === 'dev' // good