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

基于 VitePress 构建