跳转到内容

插件的环境 API

实验性功能

环境 API 是实验性的。我们仍然会在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验并在此基础上进行构建。我们计划在未来的主要版本中稳定这些新 API(可能会有破坏性更改),以便下游项目有时间试验新功能并验证它们。

资源

请与我们分享您的反馈。

在钩子中访问当前环境

鉴于 Vite 6 之前只有两个环境(clientssr),因此 ssr 布尔值足以在 Vite API 中识别当前环境。插件钩子在最后一个 options 参数中接收一个 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 拥有单独的模块图)

ts
  config(config: UserConfig) {
    config.environments.rsc ??= {}
  }

一个空对象足以注册环境,根级别环境配置中的默认值。

使用钩子配置环境

config 钩子运行时,尚未知道完整的环境列表,并且环境可能会受到根级别环境配置中的默认值或通过 config.environments 记录显式的影响。插件应使用 config 钩子设置默认值。要配置每个环境,它们可以使用新的 configEnvironment 钩子。对于每个环境都会调用此钩子,并包含其部分解析的配置,包括最终默认值的解析。

ts
  configEnvironment(name: string, options: EnvironmentOptions) {
    if (name === 'rsc') {
      options.resolve.conditions = // ...

hotUpdate 钩子

  • 类型: (this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array<EnvironmentModuleNode> | void | Promise<Array<EnvironmentModuleNode> | void>
  • 另请参阅: HMR API

hotUpdate 钩子允许插件为给定环境执行自定义 HMR 更新处理。当文件更改时,将按照 server.environments 中的顺序为每个环境串行运行 HMR 算法,因此 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
    
      // 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> 来分别保持每个环境的状态。请注意,为了向后兼容,仅为客户端环境调用 buildStartbuildEnd,而不使用 perEnvironmentStartEndDuringDev: 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 之前。

构建钩子中的环境

与开发期间相同,插件钩子也在构建期间接收环境实例,替换 ssr 布尔值。这也适用于 renderChunkgenerateBundle 和其他仅构建钩子。

构建期间的共享插件

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

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

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

在未来的主要版本中,我们可以完全对齐

在构建期间还将共享单个 ResolvedConfig 实例,从而允许在整个应用程序构建过程级别进行缓存,就像我们在开发期间使用 WeakMap<ResolvedConfig, CachedData> 所做的那样。

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

此选项最初仅适用于一小部分项目,因此插件作者可以通过将 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,
  }
}

在 MIT 许可证下发布。(083ff36d)