知海

面向插件的 Environment API

vite-mainAPI 参考

面向插件的 Environment API

:::info 候选发布版(Release Candidate)
Environment API 总体上处于候选发布阶段。我们将在各主要版本之间保持这些 API 的稳定性,以便生态系统能够进行实验并在此基础上构建。但请注意,某些特定 API 仍被视为实验性 API。

我们计划在未来的主版本中,待下游项目有足够时间体验这些新功能并加以验证后,再稳定这些新 API(可能包含破坏性变更)。

资源:

欢迎与我们分享您的反馈。

按环境划分的钩子与全局钩子 {#per-environment-hooks-and-global-hooks}

插件运行在共享的流水线中,但它们的钩子分为两类,取决于它们是面向整个服务器运行一次,还是面向每个环境各运行一次。

全局钩子只被调用一次,与所配置的环境无关。它们处理应用级的问题,例如解析配置或设置开发服务器和预览服务器,因此 this.environment 对它们不适用。与配置解析相关的钩子和与服务器相关的钩子均属于全局钩子。

按环境钩子会针对每个环境各调用一次,并通过其上下文中的 this.environment 暴露当前环境。所有 Rolldown 钩子 都是按环境调用的,其他处理模块的 Vite 特有钩子也是如此。但请注意,如果没有 perEnvironmentStartEndDuringDev: true 标志buildStartbuildEnd 仅会为客户端环境调用。

在钩子中访问当前环境

由于在 Vite 6 之前只有两个环境(clientssr),因此在 Vite API 中,一个 ssr 布尔值就足以标识当前环境。插件钩子会在最后一个选项参数中接收一个 ssr 布尔值,并且一些 API 期望一个可选的最后一个 ssr 参数,以便将模块正确关联到对应环境(例如 server.moduleGraph.getModuleByUrl(url, { ssr }))。

随着可配置环境的出现,我们现在有了一种统一的方式在插件中访问其选项和实例。插件钩子现在在其上下文中暴露 this.environment,而之前期望 ssr 布尔值的 API 现在已限定到对应的环境(例如 environment.moduleGraph.getModuleByUrl(url))。

Vite 服务器拥有一个共享的插件流水线,但在处理模块时,总是会在给定环境的上下文中进行。environment 实例在插件上下文中可用。

插件可以使用 environment 实例,根据该环境的配置(可通过 environment.config 访问)来改变模块的处理方式。

ts 复制代码
  transform(code, id) {
    console.log(this.environment.config.resolve.conditions)
  }

使用钩子注册新环境

插件可以在 config 钩子中添加新的环境。例如,RSC 支持 使用一个额外的环境,通过 react-server 条件获得独立的模块图:

ts 复制代码
  config(config: UserConfig) {
    return {
      environments: {
        rsc: {
          resolve: {
            conditions: ['react-server', ...defaultServerConditions],
          },
        },
      },
    }
  }

只需一个空对象即可注册环境,并使用根级环境配置中的默认值。

使用 configEnvironment 钩子配置环境

  • 类型: (name: string, config: EnvironmentOptions, env: { mode: string, command: 'build' | 'serve', isSsrBuild?: boolean, isPreview?: boolean, isSsrTargetWebworker?: boolean }) => EnvironmentOptions | null | void
  • 执行方式: asyncsequential
  • 作用域: 按环境

config 钩子运行时,完整的环境列表尚不可知,环境既可能受到根级环境配置的默认值影响,也可能通过 config.environments 记录显式影响。
插件应使用 config 钩子设置默认值。要为每个环境进行配置,可以使用新的 configEnvironment 钩子。该钩子会针对每个环境调用,并传入该环境部分解析后的配置,其中包含最终默认值的解析结果。

ts 复制代码
  configEnvironment(name: string, options: EnvironmentOptions) {
    // 为 rsc 环境添加 "workerd" 条件
    if (name === 'rsc') {
      return {
        resolve: {
          conditions: ['workerd'],
        },
      }
    }
  }

hotUpdate 钩子

  • 类型: (this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array<EnvironmentModuleNode> | void | Promise<Array<EnvironmentModuleNode> | void>
  • 执行方式: asyncsequential
  • 作用域: 按环境
  • 另请参阅: HMR API

hotUpdate 钩子允许插件针对给定的环境执行自定义的 HMR 更新处理。当文件发生变化时,HMR 算法会根据 server.environments 中的顺序,依次对每个环境运行,因此 hotUpdate 钩子会被多次调用。该钩子接收一个具有以下签名的上下文对象:

ts 复制代码
interface HotUpdateOptions {
  type: 'create' | 'update' | 'delete'
  file: string
  timestamp: number
  modules: Array<EnvironmentModuleNode>
  read: () => string | Promise<string>
  server: ViteDevServer
}
  • this.environment 是当前正在处理文件更新的模块执行环境。

  • modules 是此环境中受文件变更影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务的模块(例如 Vue SFC)。

  • read 是一个异步读取函数,返回文件的内容。之所以提供该函数,是因为在某些系统上,文件变更回调可能触发得过早,编辑器尚未完成文件更新,直接使用 fs.readFile 会返回空内容。传入的 read 函数会规范化这种行为。

该钩子可以选择:

  • 过滤并缩小受影响的模块列表,以便 HMR 更加精确。

  • 返回一个空数组并执行完整重载:

js 复制代码
hotUpdate({ modules, timestamp }) {
  if (this.environment.name !== 'client')
    return

  // 手动使模块失效
  const invalidatedModules = new Set()
  for (const mod of modules) {
    this.environment.moduleGraph.invalidateModule(
      mod,
      invalidatedModules,
      timestamp,
      true
    )
  }
  this.environment.hot.send({ type: 'full-reload' })
  return []
}
  • 返回一个空数组,并通过向客户端发送自定义事件来执行完全自定义的 HMR 处理:
js 复制代码
hotUpdate() {
  if (this.environment.name !== 'client')
    return

  this.environment.hot.send({
    type: 'custom',
    event: 'special-update',
    data: {}
  })
  return []
}

客户端代码应使用 HMR API 注册相应的处理程序(这可以通过同一插件的 transform 钩子注入):

js 复制代码
if (import.meta.hot) {
  import.meta.hot.on('special-update', (data) => {
    // 执行自定义更新
  })
}

插件中的按环境状态 {#per-environment-state-in-plugins}

由于同一个插件实例会被用于不同的环境,因此插件状态需要使用 this.environment 作为键。这与生态系统已经在使用的模式相同:使用 ssr 布尔值作为键来保存模块状态,以避免客户端和 ssr 模块状态混淆。可以使用 Map<Environment, State> 分别保存每个环境的状态。请注意,出于向后兼容性考虑,如果没有 perEnvironmentStartEndDuringDev: true 标志,buildStartbuildEnd 只会为客户端环境调用。如果没有 perEnvironmentWatchChangeDuringDev: true 标志,watchChange 也是如此。

js 复制代码
function PerEnvironmentCountTransformedModulesPlugin() {
  const state = new Map<Environment, { count: number }>()
  return {
    name: 'count-transformed-modules',
    perEnvironmentStartEndDuringDev: true,
    buildStart() {
      state.set(this.environment, { count: 0 })
    },
    transform(id) {
      state.get(this.environment).count++
    },
    buildEnd() {
      console.log(this.environment.name, state.get(this.environment).count)
    }
  }
}

使用 applyToEnvironment 钩子的按环境插件 {#per-environment-plugins-using-the-applytoenvironment-hook}

  • 类型: (environment: PartialEnvironment) => boolean | PluginOption | Promise<boolean>
  • 执行方式: asyncsequential
  • 作用域: 按环境

插件可以通过 applyToEnvironment 函数定义它应该应用于哪些环境。

js 复制代码
const UnoCssPlugin = () => {
  // 共享的全局状态
  return {
    buildStart() {
      // 使用 this.environment 通过 WeakMap<Environment,Data> 初始化每个环境的状态
    },
    configureServer() {
      // 正常使用全局钩子
    },
    applyToEnvironment(environment) {
      // 如果该插件应在此环境中生效,则返回 true,
      // 或者返回一个新插件来替换它。
      // 如果未使用该钩子,则插件在所有环境中均生效。
    },
    resolveId(id, importer) {
      // 仅针对该插件应用到的环境调用
    },
  }
}

如果插件不感知环境,且其状态不基于当前环境作为键,那么 applyToEnvironment 钩子可以轻松地使其按环境工作。

js 复制代码
import { nonShareablePlugin } from 'non-shareable-plugin'

export default defineConfig({
  plugins: [
    {
      name: 'per-environment-plugin',
      applyToEnvironment(environment) {
        return nonShareablePlugin({ outputName: environment.name })
      },
    },
  ],
})

Vite 导出了一个 perEnvironmentPlugin 辅助函数,以简化这些不需要其他钩子的场景:

js 复制代码
import { nonShareablePlugin } from 'non-shareable-plugin'

export default defineConfig({
  plugins: [
    perEnvironmentPlugin('per-environment-plugin', (environment) =>
      nonShareablePlugin({ outputName: environment.name }),
    ),
  ],
})

applyToEnvironment 钩子在配置阶段被调用,目前是在 configResolved 之后,因为生态系统中的项目会在其中修改插件。未来的版本中,环境插件的解析可能会移至 configResolved 之前。

应用与插件通信

environment.hot 允许插件与给定环境的应用侧代码进行通信。这相当于 客户端-服务器通信功能,但支持客户端环境以外的其他环境。warning 注意

请注意,此功能仅适用于支持 HMR 的环境。

:::

管理应用实例

请注意,同一环境中可能运行着多个应用实例。例如,如果浏览器中打开了多个标签页,那么每个标签页都是一个独立的应用实例,并且与服务器有独立的连接。

当建立新连接时,会在环境的 hot 实例上发出 vite:client:connect 事件。当连接关闭时,会发出 vite:client:disconnect 事件。

每个事件处理程序都会接收 NormalizedHotChannelClient 作为第二个参数。该客户端是一个带有 send 方法的对象,可用于向该特定应用实例发送消息。同一个连接的客户端引用始终相同,因此您可以保存它来跟踪该连接。

示例用法

插件端:

js 复制代码
configureServer(server) {
  server.environments.ssr.hot.on('my:greetings', (data, client) => {
    // 对数据做一些处理,
    // 并可选地向该应用实例发送响应
    client.send('my:foo:reply', `Hello from server! You said: ${data}`)
  })

  // 向所有应用实例广播消息
  server.environments.ssr.hot.send('my:foo', 'Hello from server!')
}

应用端与客户端-服务器通信功能相同。您可以使用 import.meta.hot 对象向插件发送消息。

构建钩子中的环境

与开发期间类似,插件钩子在构建期间也会接收环境实例,取代 ssr 布尔值。这也适用于 renderChunkgenerateBundle 和其他仅限构建的钩子。

构建期间的共享插件

在 Vite 6 之前,插件流水线在开发和生产构建期间的工作方式不同:

  • 开发期间: 插件是共享的
  • 构建期间: 插件针对每个环境隔离(在不同进程中:先 vite build,再 vite build --ssr)。

这迫使框架通过写入文件系统的 manifest 文件在 client 构建和 ssr 构建之间共享状态。在 Vite 6 中,我们现在在单个进程中构建所有环境,因此插件流水线和环境间通信的方式可以与开发期间保持一致。

在未来的主版本中,我们可能会完全对齐:

  • 在开发和生产构建期间: 插件都是共享的,并通过 按环境过滤

此外,在构建期间还将共享一个 ResolvedConfig 实例,从而可以在整个应用构建过程层面进行缓存,就像我们在开发期间使用 WeakMap<ResolvedConfig, CachedData> 所做的那样。

对于 Vite 6,我们需要采取更小的一步来保持向后兼容性。生态系统插件目前使用 config.build 而不是 environment.config.build 来访问配置,因此我们需要默认按环境创建新的 ResolvedConfig。项目可以通过将 builder.sharedConfigBuild 设置为 true 来选择共享完整的配置和插件流水线。

这个选项起初只适用于一小部分项目,因此插件作者可以通过将 sharedDuringBuild 标志设置为 true,选择让特定插件被共享。这样可以轻松地在常规插件之间共享状态:

js 复制代码
function myPlugin() {
  // 在开发和生产构建中,在所有环境之间共享状态
  const sharedState = ...
  return {
    name: 'shared-plugin',
    transform(code, id) { ... },

    // 选择为所有环境使用单一实例
    sharedDuringBuild: true,
  }
}

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。