插件环境 API
候选发布版本 (Release Candidate)
环境 API 目前处于候选发布阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验和构建。但请注意,某些特定 API 仍被视为实验性的。
一旦下游项目有时间试验这些新功能并完成验证,我们计划在未来的主要版本中稳定这些新 API(可能会有破坏性变更)。
资源
请与我们分享您的反馈。
在钩子(Hooks)中访问当前环境
鉴于在 Vite 6 之前只有两个环境(client 和 ssr),一个 ssr 布尔值足以识别 Vite API 中的当前环境。插件钩子在最后一个选项参数中接收一个 ssr 布尔值,并且许多 API 期望一个可选的最后一个 ssr 参数来将模块正确关联到对应的环境(例如 server.moduleGraph.getModuleByUrl(url, { ssr }))。
随着可配置环境的出现,我们现在有了一种统一的方式来在插件中访问其选项和实例。插件钩子现在在上下文中暴露了 this.environment,而之前期望 ssr 布尔值的 API 现在被限制在适当的环境中(例如 environment.moduleGraph.getModuleByUrl(url))。
Vite 服务器具有共享的插件管道,但处理模块时始终在特定环境的上下文中进行。environment 实例可在插件上下文中获取。
插件可以使用 environment 实例根据该环境的配置(可通过 environment.config 访问)来更改处理模块的方式。
transform(code, id) {
console.log(this.environment.config.resolve.conditions)
}使用钩子注册新环境
插件可以在 config 钩子中添加新环境。例如,RSC 支持使用了一个额外的环境,以便拥有一个带有 react-server 条件的独立模块图。
config(config: UserConfig) {
return {
environments: {
rsc: {
resolve: {
conditions: ['react-server', ...defaultServerConditions],
},
},
},
}
}使用来自根级别环境配置的默认值,只需一个空对象即可注册环境。
使用钩子配置环境
当 config 钩子运行时,环境的完整列表尚不清楚,且环境既可以受到根级别环境配置默认值的影响,也可以通过 config.environments 记录显式配置。插件应使用 config 钩子设置默认值。要配置每个环境,它们可以使用新的 configEnvironment 钩子。该钩子针对每个环境调用,并带有其部分解析后的配置,包括最终默认值的解析。
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 钩子会被多次调用。该钩子接收一个带有以下签名的上下文对象:
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 更加准确。
返回一个空数组并执行完全重新加载
jshotUpdate({ 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 处理
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) => { // perform custom update }) }
插件中的每环境状态
鉴于同一个插件实例用于不同的环境,插件状态需要以 this.environment 为键。这与生态系统已经使用的模式相同,即使用 ssr 布尔值作为键来保持模块状态,以避免混合客户端和 SSR 模块状态。可以使用 Map<Environment, State> 来分别为每个环境保留状态。请注意,为了向后兼容,除非设置了 perEnvironmentStartEndDuringDev: true 标志,否则 buildStart 和 buildEnd 仅针对客户端环境调用。对于 watchChange 和 perEnvironmentWatchChangeDuringDev: true 标志也是如此。
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 函数定义它应该应用于哪些环境。
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 钩子可以轻松地将其转换为每环境插件。
import { nonShareablePlugin } from 'non-shareable-plugin'
export default defineConfig({
plugins: [
{
name: 'per-environment-plugin',
applyToEnvironment(environment) {
return nonShareablePlugin({ outputName: environment.name })
},
},
],
})Vite 导出了一个 perEnvironmentPlugin 助手来简化那些不需要其他钩子的情况。
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 方法的对象,可用于向特定的应用程序实例发送消息。客户端引用对于同一连接始终相同,因此您可以保留它以跟踪连接。
示例用法
插件端
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 布尔值。这也适用于 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,以选择让特定插件实现共享。这使得常规插件可以轻松共享状态。
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,
}
}