面向插件的 Environment API
面向插件的 Environment API
:::info 候选发布版(Release Candidate)
Environment API 总体上处于候选发布阶段。我们将在各主要版本之间保持这些 API 的稳定性,以便生态系统能够进行实验并在此基础上构建。但请注意,某些特定 API 仍被视为实验性 API。
我们计划在未来的主版本中,待下游项目有足够时间体验这些新功能并加以验证后,再稳定这些新 API(可能包含破坏性变更)。
资源:
- 反馈讨论:我们在此收集关于新 API 的反馈。
- Environment API PR:新 API 在此实现并进行了审查。
欢迎与我们分享您的反馈。
按环境划分的钩子与全局钩子 {#per-environment-hooks-and-global-hooks}
插件运行在共享的流水线中,但它们的钩子分为两类,取决于它们是面向整个服务器运行一次,还是面向每个环境各运行一次。
全局钩子只被调用一次,与所配置的环境无关。它们处理应用级的问题,例如解析配置或设置开发服务器和预览服务器,因此
this.environment对它们不适用。与配置解析相关的钩子和与服务器相关的钩子均属于全局钩子。按环境钩子会针对每个环境各调用一次,并通过其上下文中的
this.environment暴露当前环境。所有 Rolldown 钩子 都是按环境调用的,其他处理模块的 Vite 特有钩子也是如此。但请注意,如果没有perEnvironmentStartEndDuringDev: true标志,buildStart和buildEnd仅会为客户端环境调用。在钩子中访问当前环境
由于在 Vite 6 之前只有两个环境(
client和ssr),因此在 Vite API 中,一个ssr布尔值就足以标识当前环境。插件钩子会在最后一个选项参数中接收一个ssr布尔值,并且一些 API 期望一个可选的最后一个ssr参数,以便将模块正确关联到对应环境(例如server.moduleGraph.getModuleByUrl(url, { ssr }))。随着可配置环境的出现,我们现在有了一种统一的方式在插件中访问其选项和实例。插件钩子现在在其上下文中暴露
this.environment,而之前期望ssr布尔值的 API 现在已限定到对应的环境(例如environment.moduleGraph.getModuleByUrl(url))。Vite 服务器拥有一个共享的插件流水线,但在处理模块时,总是会在给定环境的上下文中进行。
environment实例在插件上下文中可用。插件可以使用
environment实例,根据该环境的配置(可通过environment.config访问)来改变模块的处理方式。
tstransform(code, id) { console.log(this.environment.config.resolve.conditions) }使用钩子注册新环境
插件可以在
config钩子中添加新的环境。例如,RSC 支持 使用一个额外的环境,通过react-server条件获得独立的模块图:
tsconfig(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- 执行方式:
async、sequential- 作用域: 按环境
当
config钩子运行时,完整的环境列表尚不可知,环境既可能受到根级环境配置的默认值影响,也可能通过config.environments记录显式影响。
插件应使用config钩子设置默认值。要为每个环境进行配置,可以使用新的configEnvironment钩子。该钩子会针对每个环境调用,并传入该环境部分解析后的配置,其中包含最终默认值的解析结果。
tsconfigEnvironment(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>- 执行方式:
async、sequential- 作用域: 按环境
- 另请参阅: HMR API
hotUpdate钩子允许插件针对给定的环境执行自定义的 HMR 更新处理。当文件发生变化时,HMR 算法会根据server.environments中的顺序,依次对每个环境运行,因此hotUpdate钩子会被多次调用。该钩子接收一个具有以下签名的上下文对象:
tsinterface 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 更加精确。
返回一个空数组并执行完整重载:
jshotUpdate({ 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 处理:
jshotUpdate() { if (this.environment.name !== 'client') return this.environment.hot.send({ type: 'custom', event: 'special-update', data: {} }) return [] }客户端代码应使用 HMR API 注册相应的处理程序(这可以通过同一插件的
transform钩子注入):
jsif (import.meta.hot) { import.meta.hot.on('special-update', (data) => { // 执行自定义更新 }) }插件中的按环境状态 {#per-environment-state-in-plugins}
由于同一个插件实例会被用于不同的环境,因此插件状态需要使用
this.environment作为键。这与生态系统已经在使用的模式相同:使用ssr布尔值作为键来保存模块状态,以避免客户端和 ssr 模块状态混淆。可以使用Map<Environment, State>分别保存每个环境的状态。请注意,出于向后兼容性考虑,如果没有perEnvironmentStartEndDuringDev: true标志,buildStart和buildEnd只会为客户端环境调用。如果没有perEnvironmentWatchChangeDuringDev: true标志,watchChange也是如此。
jsfunction 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>- 执行方式:
async、sequential- 作用域: 按环境
插件可以通过
applyToEnvironment函数定义它应该应用于哪些环境。
jsconst UnoCssPlugin = () => { // 共享的全局状态 return { buildStart() { // 使用 this.environment 通过 WeakMap<Environment,Data> 初始化每个环境的状态 }, configureServer() { // 正常使用全局钩子 }, applyToEnvironment(environment) { // 如果该插件应在此环境中生效,则返回 true, // 或者返回一个新插件来替换它。 // 如果未使用该钩子,则插件在所有环境中均生效。 }, resolveId(id, importer) { // 仅针对该插件应用到的环境调用 }, } }如果插件不感知环境,且其状态不基于当前环境作为键,那么
applyToEnvironment钩子可以轻松地使其按环境工作。
jsimport { nonShareablePlugin } from 'non-shareable-plugin' export default defineConfig({ plugins: [ { name: 'per-environment-plugin', applyToEnvironment(environment) { return nonShareablePlugin({ outputName: environment.name }) }, }, ], })Vite 导出了一个
perEnvironmentPlugin辅助函数,以简化这些不需要其他钩子的场景:
jsimport { 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 布尔值。这也适用于 renderChunk、generateBundle 和其他仅限构建的钩子。
构建期间的共享插件
在 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,
}
}
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
