Vite 插件 HMR Hook 迁移指南:从 handleHotUpdate 走向环境感知的 hotUpdate
【免费下载链接】viteNext generation frontend tooling. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vi/vite
本文围绕 Vite 仓库中的变更说明 docs/changes/hotupdate-hook.md 展开,面向 Vite 插件作者讲清楚三件事:为什么要用新的hotUpdateHook 替代handleHotUpdate、两者的接口与行为差异、以及可直接照做的迁移模式;并结合 packages/vite/src/node/server/hmr.ts 与 packages/vite/src/node/plugin.ts 的源码实现,还原 HMR 更新在各 Environment 中的真实执行顺序,帮助你在插件中正确定制 HMR 传播策略。
背景与动机
Vite 的 Environment API 把开发服务器拆分为多个模块执行环境(client、ssr 以及框架自定义的环境)。但旧版的handleHotUpdateHook 对所有环境只调用一次,传入的HmrContext中的模块列表混合了 Client 与 SSR 两个环境的ModuleNode。文档原文给出了这个旧接口的完整签名:
interface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer }随着框架逐步迁移到自定义环境,"一次调用 + 混合模块列表"的模型不再够用。官方计划弃用handleHotUpdate,改用对每个 Environment 各调用一次的hotUpdateHook:
interface HotUpdateOptions { type: 'create' | 'update' | 'delete' file: string timestamp: number modules: Array<EnvironmentModuleNode> read: () => string | Promise<string> server: ViteDevServer }hotUpdate与handleHotUpdate的工作方式相同,但有三点关键区别:
- 按环境调用:Hook 会在每个 Environment 上各执行一次,通过
this.environment获取当前正在处理更新的 DevEnvironment。 - 模块列表更纯净:
options.modules只包含当前环境的EnvironmentModuleNode,各环境可以定义完全不同的更新策略。 - 覆盖更多 watch 事件:不再只针对
'update',还会在文件创建('create')与删除('delete')时触发,用type字段区分。
弃用时间表与试用方式
变更说明中的警告框明确标注了当前状态:
hotUpdate最早在v6.0中引入;handleHotUpdate的弃用计划在未来某个主版本(future major)进行;- 官方目前并不建议立即放弃
handleHotUpdate,插件作者可以继续留在旧 Hook 上; - 如果想提前试验并给官方反馈,可以在 vite 配置中使用
future.removePluginHookHandleHotUpdate: 'warn'。
这个配置项在源码中定义于 config.ts 的FutureOptions接口(第 581 行起),取值仅有'warn'一种;配置future: 'warn'(字符串简写)时会一次性启用所有 future 弃用告警,removePluginHookHandleHotUpdate也在其中:
// vite.config.js export default { future: { removePluginHookHandleHotUpdate: 'warn' } }启用后,只要还有插件在使用handleHotUpdate,开发服务器就会输出告警。告警的产生逻辑在 deprecations.ts 中:warnFutureDeprecation检查config.future[type] !== 'warn'时静默返回,命中则输出黄字提示 "Plugin hookhandleHotUpdate()is replaced withhotUpdate().",并附上变更说明文档路径(docs/changes/hotupdate-hook,与本文对应的正是同一份文档)和调用栈,帮助插件作者定位到具体是哪一个插件触发了旧 Hook。触发点在 hmr.ts 中,仅当type === 'update'且插件没有定义hotUpdate时才走旧分支。
迁移指南:三种典型模式
官方迁移指南的核心思想是:过滤并收窄受影响的模块列表,让 HMR 更精准。下面三种模式与原文档一一对应,可直接照搬改写。
模式一:过滤模块列表(最简单的迁移)
handleHotUpdate({ modules }) { return modules.filter(condition) } // Migrate to: hotUpdate({ modules }) { return modules.filter(condition) }两个 Hook 的签名在这个场景下几乎一致,只是modules元素类型从ModuleNode变为EnvironmentModuleNode。注意由于新 Hook 按环境调用,你可以在过滤条件里结合this.environment.name让不同环境执行不同的收窄逻辑。
模式二:返回空数组 + 手动失效模块并全量刷新
handleHotUpdate({ server, modules, timestamp }) { // Invalidate modules manually const invalidatedModules = new Set() for (const mod of modules) { server.moduleGraph.invalidateModule( mod, invalidatedModules, timestamp, true ) } server.ws.send({ type: 'full-reload' }) return [] } // Migrate to: hotUpdate({ modules, timestamp }) { // 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 [] }注意两处替换:server.moduleGraph变为this.environment.moduleGraph,server.ws.send变为this.environment.hot.send。官方 Environment Plugins 文档 中的示例还多了一个防御性写法——先判断this.environment.name !== 'client'就return,因为full-reload通常只需要发给客户端环境;如果你的全量刷新确实需要波及 SSR 等环境,去掉该判断即可。
模式三:返回空数组 + 发送自定义事件
handleHotUpdate({ server }) { server.ws.send({ type: 'custom', event: 'special-update', data: {} }) return [] } // Migrate to... hotUpdate() { this.environment.hot.send({ type: 'custom', event: 'special-update', data: {} }) return [] }客户端代码需要注册对应的处理函数(可通过同一插件的transformHook 注入),见 HMR API 文档:
if (import.meta.hot) { import.meta.hot.on('special-update', (data) => { // perform custom update }) }补充一点返回值语义(来自 plugin.ts 的 Hook 文档注释):返回过滤后的数组表示收窄更新范围;返回空数组表示"已完全自定义处理";不返回任何值(void)则按 Vite 默认 HMR 流程正常执行——这也是新旧 Hook 共有的三种返回语义。
源码深潜:hotUpdate 的实际执行链路
阅读 handleHMRUpdate 函数 可以还原出一次文件变更后的完整链路,对理解"按环境调用"有直接帮助。
1. 按环境构建 HotUpdateOptions。文件变更(create/delete/update)被监听器捕获后,函数先构造共享的contextMeta(type、file、timestamp、read、server),然后遍历每个 Environment,用各自的environment.moduleGraph.getModulesByFile(file)取受影响模块,为每个环境生成独立的options存入hotMap(第 480-497 行)。一个值得注意的细节:当type === 'create'时,moduleGraph._hasResolveFailedErrorModules中的模块会被一并加入——也就是"之前解析失败、现在文件终于创建出来了"的模块,这正是旧 Hook 拿不到的能力。
2. 先跑 client 环境的 hotUpdate,再跑其余环境。源码中 client 环境被单独提到循环之前处理(第 516-595 行),其余环境在第二个循环中逐个处理(第 601-622 行)。在 client 循环内:
if (plugin.hotUpdate) { const filteredModules = await getHookHandler(plugin.hotUpdate).call( clientContext, clientHotUpdateOptions, ) // ... } else if (type === 'update') { warnFutureDeprecation(config, 'removePluginHookHandleHotUpdate', ...) // 调用 plugin.handleHotUpdate(旧 Hook) }即同一个插件只会走新 Hook 或旧 Hook 之一,且每个环境使用的都是该环境自己的插件容器上下文(environment.pluginContainer.minimalContext),这就是this.environment的来历。
3. 新旧模块列表的双向同步。为了让旧 Hook 的语义不丢失,当hotUpdate返回了过滤结果时,源码会把过滤结果反向写回mixedHmrContext.modules(旧 Hook 的上下文):先按 id 过滤掉不在新列表中的模块,再把新列表中混合列表还没有的模块转换为向后兼容的ModuleNode补进去(第 523-545 行);反之旧handleHotUpdate返回的过滤结果也会同步回各环境的HotUpdateOptions.modules(第 559-593 行)。从源码结构看,这是一层过渡期的兼容桥——未来handleHotUpdate移除后,这条桥会随之删除。
4. 插件执行顺序与缓存。参与 HMR 的插件按pre/ normal /post排序,由 getSortedPluginsByHotUpdateHook 完成,排序依据是plugin.hotUpdate ?? plugin.handleHotUpdate,并按 Environment 缓存在WeakMap中(第 401-409 行),避免每次 Hook 调用重复建数组。
5. 各环境的 HMR 传播默认并行。所有插件 Hook 跑完后,真正的模块失效与更新传播由hmr(environment)内部调用updateModules完成,而各环境之间的调度交给可配置的 server.hotUpdateEnvironments:
hotUpdateEnvironments?: ( server: ViteDevServer, hmr: (environment: DevEnvironment) => Promise<void>, ) => Promise<void>缺省时对所有 Environment 执行Promise.all并行传播(hmr.ts 第 667-678 行)。如果你的 HMR 策略要求环境之间串行或有依赖顺序,可以实现这个回调,例如按server.environments的顺序逐个await hmr(env)。仓库的测试 hmr.spec.ts 就演示了自定义hotUpdateEnvironments的用法,并验证了服务器重启期间不会调度过期的 HMR 任务。
另外两个边界行为值得插件作者知晓:被监听文件命中 config 等敏感路径时服务器会直接重启而不进入 HMR 流程;命中experimental.bundledDev时整个 Hook 链会被跳过(源码中标注了TODO: support handleHotUpdate / hotUpdate),即 bundled dev 模式下这两个 Hook 当前不生效。
插件作者行动建议
- 现阶段:继续使用
handleHotUpdate是安全的,官方明确不建议现在迁移。 - 需要响应 create/delete 事件的插件(例如虚拟模块、按文件存在性切换行为的插件):这些能力旧 Hook 提供不了,应直接采用
hotUpdate,并用options.type区分事件。 - 想提前排雷:打开
future.removePluginHookHandleHotUpdate: 'warn',观察控制台告警与调用栈,确认插件内部及依赖的所有插件都已覆盖。 - 多环境策略:迁移后务必用
this.environment.name/this.environment.config.consumer分支处理不同环境的差异,而不是沿用旧时代"混合模块列表 + 一个ssr布尔值"的判断方式。
综上,hotUpdate是 Vite 向 Environment API 收敛 HMR 能力的第一步:接口语义与handleHotUpdate一脉相承,迁移成本主要体现在server.ws/server.moduleGraph到this.environment.hot/this.environment.moduleGraph的机械替换上;而"按环境调用 + create/delete 事件 +server.hotUpdateEnvironments调度"才是它真正为多环境开发服务器带来的增量能力。
【免费下载链接】viteNext generation frontend tooling. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vi/vite
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考