Vite Environment API for Plugins 实践:this.environment、Per-environment Hooks 与 applyToEnvironment
Vite 的 Environment API 把原来围绕 client / ssr 两个环境的 ssr 布尔参数体系,升级为统一的“环境实例”模型:插件管线在应用层面共享,但钩子会针对每个环境(environment)分别执行,并通过 this.environment 暴露当前环境。本篇基于 Vite 仓库中的 docs/guide/api-environment-plugins.md 官方文档,结合 packages/vite/src/node/plugin.ts、packages/vite/src/node/config.ts 等源码实现,完整讲解插件作者如何利用这套 API 编写多环境感知的插件——包括注册自定义环境、按环境配置选项、处理 HMR、维护 per-environment 状态,以及用 applyToEnvironment 把“不可共享”的插件变成按环境隔离的插件。
一、Environment API 的当前状态:Release Candidate
按 docs/guide/api-environment-plugins.md 的说明,Environment API 目前处于 release candidate(发布候选)阶段:Vite 会在大版本之间保持这些 API 的稳定,以便生态进行试验和构建;但其中一部分具体 API仍被视为实验性。官方计划在未来某个大版本中(可能伴随破坏性变更)正式稳定这些 API。
这一点从源码中也能得到印证。packages/vite/src/node/plugin.ts 中,applyToEnvironment、perEnvironmentPlugin、sharedDuringBuild、perEnvironmentStartEndDuringDev 等成员均标注了 @experimental(见 plugin.ts L176-L229)。此外,仓库提供了迁移期的“告警开关”:如果你正在从旧的 server.* API 迁移,可以在配置中设置 future 选项(如 removeServerModuleGraph: 'warn'、removeServerTransformRequest: 'warn' 等)来识别自己代码中对旧 API 的引用,详见 迁移指南。
适用前提:Environment 实例自 Vite 6.0 引入。阅读本文时请以当前仓库的实际实现为准;对旧版本 Vite(6 之前),只能存在 client 与 ssr 两个环境,相关 API 以 ssr 布尔参数标识环境。
二、Per-environment Hooks 与 Global Hooks 的区分
插件运行在一条共享的管线(shared pipeline)上,但钩子分为两类:
- Global hooks(全局钩子):只调用一次,与配置了哪些环境无关。它们处理应用级的事务,比如解析配置(
config、configResolved)、搭建 dev 和 preview 服务器(configureServer、configurePreviewServer等)。对这类钩子而言,this.environment没有意义——PluginContextExtension虽给上下文注入了environment字段(plugin.ts L62-L67),但Plugin.ts头部注释明确说明:当前环境只在所有非全局钩子的上下文中可用,config、configResolved、configureServer等全局钩子里拿不到。 - Per-environment hooks(每环境钩子):每个环境各调用一次,并在上下文中通过
this.environment暴露当前环境实例。所有 Rolldown 钩子(resolveId、load、transform、renderChunk等)都属于此类,以及其他处理模块的 Vite 特有钩子。
有一个重要的例外需要注意:buildStart 和 buildEnd 默认只在 client 环境被调用一次,除非插件显式声明 perEnvironmentStartEndDuringDev: true;watchChange 同理,需要 perEnvironmentWatchChangeDuringDev: true 才会按环境调用。这两个 opt-in 标志的定义和注释见 plugin.ts L185-L200,注释说明这是为向后兼容而设计的渐进式迁移路径。
三、在钩子中访问当前环境:this.environment
在 Vite 6 之前只有 client 和 ssr 两个环境,所以 Vite API 用一个 ssr 布尔值即可标识当前环境:插件钩子的最后一个 options 参数带 ssr 字段,许多 API 也接受一个可选的尾部 ssr 参数(如 server.moduleGraph.getModuleByUrl(url, { ssr }))。
引入可配置环境后,Vite 提供了一套统一方式来访问环境选项和实例:
- 插件钩子的上下文现在暴露
this.environment; - 之前接受
ssr布尔的 API,现在被收敛到各自的环境实例上,例如environment.moduleGraph.getModuleByUrl(url),无需再传ssr。
Vite 服务器拥有一条共享插件管线,但模块被处理时始终处于某个具体环境的上下文中。插件可以用环境实例(其配置可通过 environment.config 访问)来改变模块处理方式:
transform(code, id) {
console.log(this.environment.config.resolve.conditions)
}
从源码结构看,这正是 Plugin 接口继承 Rolldown 插件接口并在 PluginContext 上扩展了 environment: Environment 字段的直接结果(plugin.ts L62-L67、L85-L89 中对 rolldown 模块类型的扩展声明)。插件还可以用 this.environment.mode === 'dev' 之类的判断来守护 dev 专属逻辑。
四、在 config 钩子中注册新环境
插件可以在 config 钩子里添加新环境。文档中的例子是 RSC 支持(@vitejs/plugin-rsc):通过一个额外的环境获得独立的模块图,并使用 react-server 解析条件:
config(config: UserConfig) {
return {
environments: {
rsc: {
resolve: {
conditions: ['react-server', ...defaultServerConditions],
},
},
},
}
}
只需要返回一个空对象即可注册环境,其余配置会回落到根级别环境配置的默认值。也就是说,environments 是一个“名字 → EnvironmentOptions”的记录,client、ssr 是内置环境,其余名字都可以由插件或用户配置按需添加。
五、configEnvironment 钩子:按环境配置选项
- 类型:
(name: string, config: EnvironmentOptions, env: { mode: string, command: 'build' | 'serve', isSsrBuild?: boolean, isPreview?: boolean, isSsrTargetWebworker?: boolean }) => EnvironmentOptions | null | void - 种类:
async、sequential - 作用域: Per-environment
config 钩子运行时,完整的环境列表还未知,环境既可能受到根级环境配置默认值的影响,也可能被 config.environments 记录显式指定。因此官方给出的分工是:用 config 钩子设置默认值,用 configEnvironment 钩子配置每个环境——后者针对每个环境各调用一次,拿到的是部分解析(含最终默认值解析)后的配置。
configEnvironment(name: string, options: EnvironmentOptions) {
// add "workerd" condition to the rsc environment
if (name === 'rsc') {
return {
resolve: {
conditions: ['workerd'],
},
}
}
}
源码实现位于 config.ts 的 runConfigEnvironmentHook:它先取所有环境名,然后按 getSortedPluginsByHook('configEnvironment', plugins) 的插件顺序依次执行;对每个插件,遍历所有环境名调用钩子,若返回非空结果则用 mergeConfig 深合并回该环境的选项。这也解释了“每个环境各调用一次”的语义:外层是插件顺序(sequential),内层是环境遍历。
六、hotUpdate 钩子:按环境的 HMR 更新处理
- 类型:
(this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array<EnvironmentModuleNode> | void | Promise<Array<EnvironmentModuleNode> | void> - 种类:
async、sequential - 作用域: Per-environment
- 相关文档: 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会读到空内容。这个 read 函数对该行为做了归一化。
钩子有三种典型处理方式:
1. 过滤并收窄受影响的模块列表,让 HMR 更精准——返回过滤后的模块数组即可。
2. 返回空数组并执行整页刷新(full reload):
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 []
}
注意这里的模块失效操作走的是 this.environment.moduleGraph.invalidateModule,即环境实例上的模块图 API,而不是旧的 server.moduleGraph。
3. 返回空数组并完全自定义 HMR 处理——向客户端发送自定义事件:
hotUpdate() {
if (this.environment.name !== 'client')
return
this.environment.hot.send({
type: 'custom',
event: 'special-update',
data: {}
})
return []
}
客户端代码需要注册对应的事件处理器(可以注入到同插件的 transform 钩子中),使用 HMR API:
if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
// perform custom update
})
}
七、插件的 Per-environment 状态
由于同一个插件实例会被用于不同环境,插件内部的状态必须以 this.environment 为键,避免各环境的状态互相污染。这正是生态此前用 ssr 布尔作键来区分 client/ssr 模块状态的同一模式的推广:可以用 Map<Environment, State> 为每个环境维护独立状态。再强调一次:buildStart / buildEnd 默认只在 client 环境调用(除非 perEnvironmentStartEndDuringDev: true),watchChange 同理(perEnvironmentWatchChangeDuringDev: true)。
文档给出的完整示例是统计每个环境被 transform 的模块数量:
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)
}
}
}
这个示例同时演示了 perEnvironmentStartEndDuringDev: true 的用法:声明后 buildStart/buildEnd 会在 dev 期间按环境调用,使状态初始化与收尾逻辑能覆盖所有环境,而不仅仅是 client。
八、applyToEnvironment 钩子与 perEnvironmentPlugin 助手
- 类型:
(environment: PartialEnvironment) => boolean | PluginOption | Promise<boolean> - 种类:
async、sequential - 作用域: Per-environment
插件可以用 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
},
}
}
返回值的三种语义:
- 返回
true:插件在该环境激活; - 返回
false(或 falsy):插件在该环境不激活; - 返回一个
PluginOption(可以是数组、false等):用一个新插件替换原插件。若没有定义applyToEnvironment,插件默认在所有环境激活。
源码实现 resolveEnvironmentPlugins 与上述语义一一对应:遍历顶层配置的插件,对有 applyToEnvironment 的插件先 await 其返回值;falsy 则 continue 跳过;非 true 则 asyncFlatten(arraify(applied)) 展开后压入该环境的插件列表;true 则保留原插件。
如果某个插件不感知环境、且其内部状态没有按当前环境分键,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 }),
),
],
})
它的实现非常薄,见 plugin.ts L431-L441:就是构造一个只带 name 和 applyToEnvironment 两个键的对象,并从 vite 包入口导出(index.ts L35)。值得注意的是,Vite 自己内部也大量使用了这套机制——例如 reporter、manifest、wasm-helper、native:import-analysis-build 等内置插件均通过 perEnvironmentPlugin 定义(见 plugins/reporter.ts、plugins/manifest.ts),而 css、resolve、html、define、esbuild 等核心内置插件则使用 applyToEnvironment 控制自身在不同环境的启用条件。
时序上有一个注意点:applyToEnvironment 钩子在配置解析期(config time)调用,目前位于 configResolved 之后(因为生态项目可能在那之前修改插件列表);从源码结构看,环境插件解析在未来可能被移到 configResolved 之前。
九、应用与插件的通信:environment.hot
environment.hot 允许插件与“给定环境”中运行在应用侧的代码通信。它是 Client-server Communication 特性的对应物,但支持 client 以外的环境。
注意:该特性仅对支持 HMR 的环境可用。
管理应用实例
同一个环境中可能同时运行多个应用实例。例如浏览器里开了多个标签页,每个标签页都是一个独立的应用实例,各自与服务端保持一条独立连接。
- 当新连接建立时,环境
hot实例上会发出vite:client:connect事件; - 当连接关闭时,发出
vite:client:disconnect事件。
每个事件处理器接收 NormalizedHotChannelClient 作为第二个参数。client 是一个带 send 方法的对象,可用于向该特定应用实例发送消息。同一连接的 client 引用始终相同,因此可以保存它来追踪连接。
使用示例
插件侧:
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!')
}
应用侧与 Client-server Communication 特性相同:用 import.meta.hot 对象向插件发送消息、注册事件处理。
十、构建时的环境:Build Hooks 与共享插件管线
Build Hooks 中的环境
与 dev 一样,构建期插件钩子也会收到环境实例,取代了原来的 ssr 布尔。这对 renderChunk、generateBundle 等仅在构建期存在的钩子同样适用——它们同样以 this.environment 访问当前构建环境。
Shared Plugins During Build 的演进
Vite 6 之前,插件管线在 dev 和 build 中的工作方式不同:
- Dev 期间: 插件共享(一条管线跑完所有环境);
- Build 期间: 插件按环境隔离(在不同进程中运行:先
vite build,再vite build --ssr)。
这迫使框架通过写入文件系统的 manifest 文件来在 client 构建与 ssr 构建之间共享状态。Vite 6 改为在单个进程中构建所有环境,使插件管线与环境间通信方式能够与 dev 对齐。未来大版本中可能做到完全对齐:
- dev 与 build 均: 插件共享,并支持按环境过滤;
- 构建期间只有一个共享的
ResolvedConfig实例,从而可以在“整个应用构建流程”级别做缓存——就像 dev 期间用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,
}
}
源码中 sharedDuringBuild 的注释(plugin.ts L176-L184)补充了背景:为向后兼容,插件在 vite build --app 期间仍会为每个环境重新创建;目前是“每插件 opt-in + 全局 builder.sharedPlugins”的组合,未来大版本会把默认值翻转为“默认共享”。
十一、配套迁移:从 server.* 到 environment.*
写多环境插件时,还会碰到旧 API 的迁移问题。docs/changes/per-environment-apis.md 说明了动机与对照关系:Vite 5 及以前,一个 dev server 固定有 client 和 ssr 两个环境,server.moduleGraph 混合了两种环境的模块,节点通过 clientImportedModules / ssrImportedModules 列表关联,被转换的模块用 id + ssr 布尔表示;Vite 6 允许创建任意数量的自定义环境(client、ssr、edge 等),单个 ssr 布尔不再够用。因此这些方法被移到环境实例上,使它们可以不依赖 Vite dev server 实例直接调用。迁移对照:
| 旧 API | 新 API |
|---|---|
server.moduleGraph |
environment.moduleGraph |
server.reloadModule(module) |
environment.reloadModule(module) |
server.pluginContainer |
environment.pluginContainer |
server.transformRequest(url, ssr) |
environment.transformRequest(url) |
server.warmupRequest(url, ssr) |
environment.warmupRequest(url) |
server.hot |
server.client.environment.hot |
该文档同时提醒:旧方法的弃用计划在未来大版本执行,官方暂不推荐立刻全面迁移;可以通过上文提到的 future 配置项(如 removeServerModuleGraph: 'warn'、removeServerReloadModule: 'warn'、removeServerPluginContainer: 'warn'、removeServerHot: 'warn'、removeServerTransformRequest: 'warn'、removeServerWarmupRequest: 'warn')先以告警方式识别代码中的旧用法。
十二、小结:插件作者的多环境心智模型
- 钩子分两类:全局钩子(
config/configResolved/configureServer等,调用一次,无this.environment)与 per-environment 钩子(Rolldown 钩子等,每环境一次,带this.environment); - 注册新环境用
config钩子返回environments记录;按环境微调解析后的选项用configEnvironment钩子(插件顺序 × 环境遍历,返回值深合并); - HMR 处理用
hotUpdate+this.environment.hot/this.environment.moduleGraph; - 插件状态以
Environment为键(Map/WeakMap),必要时用perEnvironmentStartEndDuringDev/perEnvironmentWatchChangeDuringDevopt-in 全量环境调用; - 按环境启用/替换插件用
applyToEnvironment(true保留、falsy 跳过、返回插件则替换)或perEnvironmentPlugin助手; - 应用侧通信用
environment.hot的send/on与vite:client:connect/vite:client:disconnect事件; - 构建期默认每环境独立插件实例与
ResolvedConfig,可用sharedDuringBuild(插件级)与builder.sharedConfigBuild(项目级)逐步走向与 dev 一致的共享管线。
这些机制的共同目标是:让“环境”成为插件世界的一等概念,使 RSC、边缘运行时等多环境场景下的插件开发与单一环境场景同样直接。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00