《Vite 设计与实现》完整目录
第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 的类型体系、生命周期管理和多环境协作机制。
:::tip 本章要点
- 理解 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 的目标是将"环境"提升为一等公民:
(transformResult + ssrTransformResult)"] A --> C["共享 PluginContainer"] A --> D["共享 DepsOptimizer"] end subgraph "Vite 6+ (环境隔离模型)" E["ViteDevServer"] --> F["client Environment"] E --> G["ssr Environment"] E --> H["rsc Environment (自定义)"] F --> F1["ModuleGraph"] F --> F2["PluginContainer"] F --> F3["DepsOptimizer"] G --> G1["ModuleGraph"] G --> G2["PluginContainer"] G --> G3["DepsOptimizer"] H --> H1["ModuleGraph"] H --> H2["PluginContainer"] H --> H3["DepsOptimizer"] end style F fill:#e3f2fd style G fill:#e8f5e9 style H fill:#fff3e0
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
如果未来 Vite 添加新的环境类型,反向检查(!== 'build')会错误地将新类型归类为 dev。UnknownEnvironment 作为"未知类型占位符",迫使开发者使用正向检查(=== 'dev'),从而保证代码的前向兼容性。
16.3 DevEnvironment
16.3.1 核心组件
DevEnvironment 是开发阶段最重要的环境类型,它拥有完整的运行时基础设施:
typescript
export class DevEnvironment extends BaseEnvironment {
mode = 'dev' as const
// 每环境独立的模块图
moduleGraph: EnvironmentModuleGraph
// 每环境独立的依赖优化器
depsOptimizer?: DepsOptimizer
// 每环境独立的插件容器
get pluginContainer(): EnvironmentPluginContainer<DevEnvironment> {
if (!this._pluginContainer)
throw new Error(`${this.name} environment.pluginContainer called before initialized`)
return this._pluginContainer
}
// 热更新通道
hot: NormalizedHotChannel
}
16.3.2 初始化流程
DevEnvironment 的初始化是一个两阶段过程:
构造函数中的关键初始化:
typescript
constructor(name, config, context) {
super(name, config, options)
// 创建独立的模块图
this.moduleGraph = new EnvironmentModuleGraph(name, (url: string) =>
this.pluginContainer!.resolveId(url, undefined),
)
// 配置热更新通道
this.hot = context.transport
? normalizeHotChannel(context.transport, context.hot)
: normalizeHotChannel({}, context.hot)
// 注册 fetchModule 和 getBuiltins 远程调用
this.hot.setInvokeHandler({
fetchModule: (id, importer, options) =>
this.fetchModule(id, importer, options),
getBuiltins: async () =>
this.config.resolve.builtins.map(/* 序列化 */),
})
// 创建依赖优化器
if (!context.disableDepsOptimizer) {
const { optimizeDeps } = this.config
if (context.depsOptimizer) {
this.depsOptimizer = context.depsOptimizer
} else if (!isDepOptimizationDisabled(optimizeDeps)) {
this.depsOptimizer = (
optimizeDeps.noDiscovery
? createExplicitDepsOptimizer
: createDepsOptimizer
)(this)
}
}
}
16.3.3 模块图隔离
每个 DevEnvironment 拥有独立的 EnvironmentModuleGraph,这意味着:
- 客户端环境中的
import './style.css'和 SSR 环境中的import './style.css'会产生不同的模块节点 - 每个环境的模块可以有不同的
transformResult(因为插件可能根据环境产生不同的输出) - HMR 失效在环境内传播,不会跨环境影响
transformResult: 客户端 SFC"] A2["./style.css
transformResult: CSS Module"] A1 --> A2 end subgraph "ssr 环境" B1["./App.vue
transformResult: SSR SFC"] B2["./style.css
transformResult: 空 (SSR 不处理 CSS)"] B1 --> B2 end style A1 fill:#e3f2fd style B1 fill:#e8f5e9
16.3.4 热更新与失效传播
DevEnvironment 的 invalidateModule 方法处理从客户端发来的模块失效消息:
typescript
protected invalidateModule(m, _client) {
const mod = this.moduleGraph.urlToModuleMap.get(m.path)
if (
mod &&
mod.isSelfAccepting &&
mod.lastHMRTimestamp > 0 &&
!mod.lastHMRInvalidationReceived
) {
mod.lastHMRInvalidationReceived = true
this.logger.info(
colors.yellow(`hmr invalidate `) + colors.dim(m.path),
{ timestamp: true },
)
const file = getShortName(mod.file!, this.config.root)
updateModules(
this,
file,
[...mod.importers].filter((imp) => imp !== mod),
mod.lastHMRTimestamp,
m.firstInvalidatedBy,
)
}
}
lastHMRInvalidationReceived 标志防止同一模块在单次 HMR 周期内被重复失效。失效传播只沿着当前环境的模块图进行,不会跨环境。
16.4 BuildEnvironment
16.4.1 构建环境的简洁性
相比 DevEnvironment 的丰富功能,BuildEnvironment 非常简洁:
typescript
export class BuildEnvironment extends BaseEnvironment {
mode = 'build' as const
isBuilt = false
constructor(
name: string,
config: ResolvedConfig,
setup?: { options?: EnvironmentOptions },
) {
let options = config.environments[name]
if (!options) {
throw new Error(`Environment "${name}" is not defined in the config.`)
}
if (setup?.options) {
options = mergeConfig(options, setup.options) as ResolvedEnvironmentOptions
}
super(name, config, options)
}
async init(): Promise<void> {
if (this._initiated) return
this._initiated = true
}
}
BuildEnvironment 不需要模块图(构建使用 Rolldown 的内部图)、不需要 HMR、不需要依赖优化器。它的主要职责是:
- 持有环境特定的配置
- 提供环境特定的插件列表
- 作为构建函数的上下文对象
16.4.2 ViteBuilder:多环境构建编排
typescript
export interface ViteBuilder {
environments: Record<string, BuildEnvironment>
config: ResolvedConfig
buildApp(): Promise<void>
build(environment: BuildEnvironment): Promise<RolldownOutput>
}
ViteBuilder 管理所有 BuildEnvironment 实例,并通过 buildApp() 方法编排它们的构建顺序。框架可以通过 builder.buildApp 配置自定义构建逻辑,例如先构建 SSR 环境再构建客户端环境。
typescript
export interface BuilderOptions {
sharedConfigBuild?: boolean // 是否在环境间共享配置实例
sharedPlugins?: boolean // 是否在环境间共享插件实例
buildApp?: (builder: ViteBuilder) => Promise<void>
}
sharedConfigBuild 和 sharedPlugins 选项允许控制环境间的共享程度,在构建性能和隔离性之间做出权衡。
16.5 ScanEnvironment
16.5.1 依赖扫描的专用环境
ScanEnvironment(optimizer/scan.ts)是一个特殊的环境类型,专用于依赖预打包的扫描阶段:
typescript
export class ScanEnvironment extends BaseEnvironment {
mode = 'scan' as const
get pluginContainer(): EnvironmentPluginContainer {
if (!this._pluginContainer)
throw new Error(`${this.name} environment.pluginContainer called before initialized`)
return this._pluginContainer
}
_pluginContainer: EnvironmentPluginContainer | undefined
async init(): Promise<void> {
if (this._initiated) return
this._initiated = true
this._pluginContainer = await createEnvironmentPluginContainer(
this,
this.plugins,
undefined,
false, // watcher 为 undefined,不监听文件变化
)
}
}
与 DevEnvironment 不同,ScanEnvironment:
- 没有模块图(扫描不需要持久化模块信息)
- 没有 HMR 通道(扫描是一次性过程)
- 没有依赖优化器(扫描本身就是优化器的前置步骤)
- 没有文件监听器(扫描在启动时执行一次)
16.5.2 开发环境到扫描环境的降级
Vite 提供了一个 devToScanEnvironment 工具函数,将 DevEnvironment "降级"为 ScanEnvironment 的行为:
typescript
export function devToScanEnvironment(
environment: DevEnvironment,
): ScanEnvironment {
return {
mode: 'scan',
get name() { return environment.name },
getTopLevelConfig() { return environment.getTopLevelConfig() },
get config() { return environment.config },
get logger() { return environment.logger },
get pluginContainer() { return environment.pluginContainer },
get plugins() { return environment.plugins },
} as unknown as ScanEnvironment
}
这个函数创建了一个 ScanEnvironment 的"视图",它复用了 DevEnvironment 的配置和插件容器,但限制了对模块图和其他运行时设施的访问。这种"限制性代理"模式确保了扫描阶段不会意外修改开发服务器的运行时状态。
16.6 Environment 联合类型
environment.ts 定义了 Environment 的联合类型:
typescript
export type Environment =
| DevEnvironment
| BuildEnvironment
| /** @internal */ ScanEnvironment
| UnknownEnvironment
注意 ScanEnvironment 被标记为 @internal,表示它不是公开 API 的一部分。这种类型设计使得插件开发者可以通过 environment.mode 进行类型缩窄:
typescript
function handleEnvironment(environment: Environment) {
if (environment.mode === 'dev') {
// TypeScript 知道这里是 DevEnvironment
environment.moduleGraph.getModuleById(id)
} else if (environment.mode === 'build') {
// TypeScript 知道这里是 BuildEnvironment
console.log(environment.isBuilt)
}
}
16.7 perEnvironmentPlugin
16.7.1 环境感知的插件适配
perEnvironmentPlugin 是一个高阶函数,将环境感知的插件工厂转换为标准的 Vite 插件:
typescript
export function perEnvironmentPlugin(
name: string,
factory: (environment: Environment) => Plugin | Plugin[] | false | undefined,
): Plugin
它允许插件根据环境返回不同的实现,或者通过返回 false 来声明某个环境不需要该插件。例如 Manifest 插件:
typescript
return perEnvironmentPlugin('native:manifest', (environment) => {
if (!environment.config.build.manifest) return false
return [
{ name: 'native:manifest-envs', /* ... */ },
nativeManifestPlugin({ /* ... */ }),
{ name: 'native:manifest-compatible', /* ... */ },
]
})
16.7.2 perEnvironmentState
perEnvironmentState 提供了跨 Hook 的环境级状态管理:
typescript
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:当环境实例被垃圾回收时,关联的状态也会被自动清理- 惰性初始化:状态在第一次访问时创建,避免了不必要的初始化开销
- 类型安全 :通过泛型
<State>确保类型正确性 - 上下文绑定 :状态通过
PluginContext中的environment引用获取,插件无需手动管理环境映射
使用示例(Manifest 插件):
typescript
const getState = perEnvironmentState(() => ({
manifest: {} as Manifest,
outputCount: 0,
reset() {
this.manifest = {}
this.outputCount = 0
},
}))
// 在 Hook 中使用
generateBundle(_, bundle) {
const state = getState(this) // this 是 PluginContext
state.outputCount++
// ...
}
16.8 环境配置体系
16.8.1 配置声明
环境通过 config.environments 对象声明:
typescript
// vite.config.ts
export default {
environments: {
client: {
// 客户端环境配置
build: { outDir: 'dist/client' },
},
ssr: {
// SSR 环境配置
build: { outDir: 'dist/server' },
resolve: {
conditions: ['node'],
externalConditions: ['node', 'module-sync'],
},
},
rsc: {
// React Server Components 环境(自定义)
build: { outDir: 'dist/rsc' },
resolve: {
conditions: ['react-server'],
},
},
},
}
16.8.2 Proxy 配置合并的实际效果
这种 Proxy 方式的优势在于:
- 零拷贝:不需要深拷贝整个配置对象
- 惰性求值:只在属性被访问时才进行查找
- 自动回退:未覆盖的属性自动使用顶层值
getTopLevelConfig():当确实需要访问顶层配置时,可以绕过 Proxy
16.9 多环境协作模式
16.9.1 applyToEnvironment 过滤
插件可以通过 applyToEnvironment Hook 声明自己适用于哪些环境:
typescript
// 只在有 minify 配置的环境中生效
applyToEnvironment(environment) {
return !!environment.config.build.minify
}
// 只在客户端环境中生效
applyToEnvironment(environment) {
return environment.config.consumer === 'client'
}
// 只在生成 SSR Manifest 的环境中生效
applyToEnvironment(environment) {
return !!environment.config.build.ssrManifest
}
16.9.2 开发服务器中的多环境
产生不同的转换结果
16.9.3 consumer 属性
环境的 consumer 属性标识了代码的运行目标:
typescript
// 在环境配置中
consumer: 'client' | 'server'
这个属性影响多个插件的行为:
- CSS 插件在
consumer === 'server'时不注入样式代码 - Module Preload Polyfill 在
consumer !== 'client'时返回空 - WASM 插件在
consumer === 'server'时使用文件系统读取而非fetch
16.10 设计决策分析
16.10.1 为什么用 Proxy 而非深拷贝
Proxy 方式相比深拷贝有明显优势:
| 方面 | Proxy | 深拷贝 |
|---|---|---|
| 内存 | 只存储覆盖的属性 | 完整拷贝所有属性 |
| 一致性 | 顶层配置修改自动反映 | 拷贝后脱离同步 |
| 性能 | 属性访问有微小开销 | 一次性开销,后续无开销 |
| 动态性 | 支持运行时配置变更 | 不支持 |
对于 Vite 的场景,配置对象属性众多但每个环境只覆盖少量属性,Proxy 方式在内存效率和一致性上都更优。
16.10.2 WeakMap 状态管理
16.10.3 模式正向检查
UnknownEnvironment 的引入迫使开发者使用正向模式检查:
typescript
// 这段代码在添加新环境类型后仍然正确
if (env.mode === 'dev') {
// DevEnvironment 特有逻辑
} else if (env.mode === 'build') {
// BuildEnvironment 特有逻辑
} else {
// 未知类型,安全的默认行为
}
// 这段代码在添加新环境类型后可能出错
if (env.mode !== 'build') {
// 错误:新的环境类型也会进入这里
}
16.11 小结
Environment API 是 Vite 6 最重要的架构变革,它将"环境"从一个布尔参数提升为一等公民:
-
类型体系 采用四层继承设计(
PartialEnvironment -> BaseEnvironment -> Dev/Build/Scan/UnknownEnvironment),每一层增加一组能力。UnknownEnvironment作为"类型守卫"确保正向模式检查的代码风格。 -
Proxy 配置合并通过 JavaScript Proxy 实现了零拷贝的配置覆盖。环境特定的选项覆盖顶层配置,未覆盖的属性自动回退,既保证了内存效率又维持了配置一致性。
-
DevEnvironment 拥有独立的模块图、插件容器、依赖优化器和热更新通道。模块图隔离确保了不同环境对同一文件的转换互不干扰。
-
BuildEnvironment 和 ScanEnvironment 分别为构建和扫描阶段提供了精简的环境抽象,体现了"按需提供能力"的原则。
-
perEnvironmentPlugin 和 perEnvironmentState 为插件提供了环境感知的适配模式。
WeakMap状态管理确保了内存安全,惰性初始化避免了不必要的开销。 -
ViteBuilder 通过
sharedConfigBuild和sharedPlugins选项在隔离性和性能之间提供了灵活的权衡。
这套 API 使得 Vite 从"客户端 + SSR"的双模型,演进为可以支持任意数量自定义环境的通用构建编排器。框架开发者可以为 RSC、Service Worker、Edge Runtime 等场景定义独立的环境,每个环境拥有完全独立的处理管线,同时通过共享的顶层配置保持协调。