Skip to content

第16章 Environment API

开篇引言

在 Vite 6 之前,一个 Vite 服务器实例只有一个统一的模块图、插件管线和依赖优化器。当项目需要同时处理客户端代码和 SSR 代码时,这些共享的基础设施不得不通过参数(如 ssr: boolean)来区分行为。这种方式简单但脆弱 -- 当需要支持更多的运行环境(如 RSC、Service Worker、Edge Runtime)时,布尔参数无法扩展。

Vite 6 引入的 Environment API 从根本上改变了这一架构。每个"环境"拥有独立的模块图、插件容器和依赖优化器,它们通过共享的顶层配置保持协调。这不是一次简单的重构,而是 Vite 向"通用构建编排器"角色演进的关键一步。

本章将从 environment.tsbaseEnvironment.tsserver/environment.tsbuild.tsoptimizer/scan.ts 等源码文件出发,深入分析 Environment API 的类型体系、生命周期管理和多环境协作机制。

本章要点

  • 理解 Environment API 的设计动机与架构目标
  • 掌握 PartialEnvironment -> BaseEnvironment -> DevEnvironment/BuildEnvironment/ScanEnvironment 的类型层次
  • 分析 perEnvironmentPluginperEnvironmentState 的多环境插件适配模式
  • 理解每环境独立的模块图、插件容器和依赖优化器
  • 掌握环境配置的 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)

这种方式存在几个问题:

  1. 不可扩展:当需要支持第三种环境(如 React Server Components 的 RSC 环境)时,布尔值无法表达
  2. 共享污染:客户端和 SSR 共享同一个模块图,模块的 transformResultssrTransformResult 混存在同一个节点上
  3. 优化冲突:客户端和 SSR 可能需要不同的依赖优化策略,共享的优化器无法同时满足
  4. 插件歧义:插件需要在运行时检查 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:插件层

BaseEnvironmentPartialEnvironment 基础上增加了插件访问能力和初始化状态追踪:

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 mode to determine the type of environment, e.g.

js
const 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 失效在环境内传播,不会跨环境影响

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、不需要依赖优化器。它的主要职责是:

  1. 持有环境特定的配置
  2. 提供环境特定的插件列表
  3. 作为构建函数的上下文对象

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>
}

sharedConfigBuildsharedPlugins 选项允许控制环境间的共享程度,在构建性能和隔离性之间做出权衡。

16.5 ScanEnvironment

16.5.1 依赖扫描的专用环境

ScanEnvironmentoptimizer/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
  }
}

关键设计点:

  1. WeakMap:当环境实例被垃圾回收时,关联的状态也会被自动清理
  2. 惰性初始化:状态在第一次访问时创建,避免了不必要的初始化开销
  3. 类型安全:通过泛型 <State> 确保类型正确性
  4. 上下文绑定:状态通过 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 拥有独立的模块图、插件容器、依赖优化器和热更新通道。模块图隔离确保了不同环境对同一文件的转换互不干扰。

  • BuildEnvironmentScanEnvironment 分别为构建和扫描阶段提供了精简的环境抽象,体现了"按需提供能力"的原则。

  • perEnvironmentPluginperEnvironmentState 为插件提供了环境感知的适配模式。WeakMap 状态管理确保了内存安全,惰性初始化避免了不必要的开销。

  • ViteBuilder 通过 sharedConfigBuildsharedPlugins 选项在隔离性和性能之间提供了灵活的权衡。

这套 API 使得 Vite 从"客户端 + SSR"的双模型,演进为可以支持任意数量自定义环境的通用构建编排器。框架开发者可以为 RSC、Service Worker、Edge Runtime 等场景定义独立的环境,每个环境拥有完全独立的处理管线,同时通过共享的顶层配置保持协调。

基于 VitePress 构建