跳转到内容

插件环境 API

候选发布版本 (Release Candidate)

环境 API 目前处于候选发布阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验和构建。但请注意,某些特定 API 仍被视为实验性的。

一旦下游项目有时间试验这些新功能并完成验证,我们计划在未来的主要版本中稳定这些新 API(可能会有破坏性变更)。

资源

请与我们分享您的反馈。

在钩子(Hooks)中访问当前环境

鉴于在 Vite 6 之前只有两个环境(clientssr),一个 ssr 布尔值足以识别 Vite API 中的当前环境。插件钩子在最后一个选项参数中接收一个 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],
          },
        },
      },
    }
  }

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

使用钩子配置环境

config 钩子运行时,环境的完整列表尚不清楚,且环境既可以受到根级别环境配置默认值的影响,也可以通过 config.environments 记录显式配置。插件应使用 config 钩子设置默认值。要配置每个环境,它们可以使用新的 configEnvironment 钩子。该钩子针对每个环境调用,并带有其部分解析后的配置,包括最终默认值的解析。

ts
  configEnvironment(name: string, options: EnvironmentOptions) {
    // add "workerd" condition to the rsc environment
    if (name === 'rsc') {
      return {
        resolve: {
          conditions: ['workerd'],
        },
      }
    }
  }

hotUpdate 钩子

  • 类型: (this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array<EnvironmentModuleNode> | void | Promise<Array<EnvironmentModuleNode> | void>
  • 类型: async, sequential
  • 另见: 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 会返回空内容。传入的读取函数规范化了此行为。

钩子可以选择:

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

  • 返回一个空数组并执行完全重新加载

    js
    hotUpdate({ modules, timestamp }) {
      if (this.environment.name !== 'client')
        return
    
      // Invalidate modules manually
      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) => {
        // perform custom update
      })
    }

插件中的每环境状态

鉴于同一个插件实例用于不同的环境,插件状态需要以 this.environment 为键。这与生态系统已经使用的模式相同,即使用 ssr 布尔值作为键来保持模块状态,以避免混合客户端和 SSR 模块状态。可以使用 Map<Environment, State> 来分别为每个环境保留状态。请注意,为了向后兼容,除非设置了 perEnvironmentStartEndDuringDev: true 标志,否则 buildStartbuildEnd 仅针对客户端环境调用。对于 watchChangeperEnvironmentWatchChangeDuringDev: true 标志也是如此。

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 函数定义它应该应用于哪些环境。

js
const UnoCssPlugin = () => {
  // shared global state
  return {
    buildStart() {
      // init per-environment state with WeakMap<Environment,Data>
      // using this.environment
    },
    configureServer() {
      // use global hooks normally
    },
    applyToEnvironment(environment) {
      // return true if this plugin should be active in this environment,
      // or return a new plugin to replace it.
      // if the hook is not used, the plugin is active in all environments
    },
    resolveId(id, importer) {
      // only called for environments this plugin apply to
    },
  }
}

如果插件不具备环境感知能力且其状态不是以当前环境为键,则 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 允许插件与给定环境的应用程序端代码进行通信。这等同于 客户端-服务器通信功能,但支持除客户端环境以外的其他环境。

注意

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

管理应用实例

请注意,在同一环境中可能同时运行多个应用程序实例。例如,如果您在浏览器中打开了多个标签页,每个标签页都是一个单独的应用程序实例,并与服务器有单独的连接。

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

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

示例用法

插件端

js
configureServer(server) {
  server.environments.ssr.hot.on('my:greetings', (data, client) => {
    // do something with the data,
    // and optionally send a response to that application instance
    client.send('my:foo:reply', `Hello from server! You said: ${data}`)
  })

  // broadcast a message to all application instances
  server.environments.ssr.hot.send('my:foo', 'Hello from server!')
}

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

构建钩子中的环境

与开发期间一样,插件钩子在构建期间也会接收环境实例,从而替代了 ssr 布尔值。这也适用于 renderChunkgenerateBundle 和其他仅在构建时调用的钩子。

构建期间共享插件

在 Vite 6 之前,插件管道在开发和构建期间的工作方式不同:

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

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

在未来的大版本中,我们可能会实现完全一致:

构建期间还将共享一个 ResolvedConfig 实例,从而允许在整个应用构建流程级别进行缓存,方式与我们在开发期间使用 WeakMap<ResolvedConfig, CachedData> 的方式相同。

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

该选项起初仅适用于一小部分项目,因此插件作者可以设置 sharedDuringBuild 标志为 true,以选择让特定插件实现共享。这使得常规插件可以轻松共享状态。

js
function myPlugin() {
  // Share state among all environments in dev and build
  const sharedState = ...
  return {
    name: 'shared-plugin',
    transform(code, id) { ... },

    // Opt-in into a single instance for all environments
    sharedDuringBuild: true,
  }
}