知海

HMR API

vite-mainAPI 参考

HMR API

:::tip 注意
这是客户端 HMR API。有关在插件中处理 HMR 更新,请参阅 handleHotUpdate

手动 HMR API 主要面向框架和工具作者。作为最终用户,HMR 很可能已经在框架特定的起始模板中为你处理好了。
:::

Vite 通过特殊的 import.meta.hot 对象暴露其手动 HMR API:

ts twoslash 复制代码
import type { ModuleNamespace } from 'vite/types/hot.d.ts'
import type {
  CustomEventName,
  InferCustomEventPayload,
} from 'vite/types/customEvent.d.ts'

// ---cut---
interface ImportMeta {
  readonly hot?: ViteHotContext
}

interface ViteHotContext {
  readonly data: any

  accept(): void
  accept(cb: (mod: ModuleNamespace | undefined) => void): void
  accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void
  accept(
    deps: readonly string[],
    cb: (mods: Array<ModuleNamespace | undefined>) => void,
  ): void

  dispose(cb: (data: any) => void): void
  prune(cb: (data: any) => void): void
  invalidate(message?: string): void

  on<T extends CustomEventName>(
    event: T,
    cb: (payload: InferCustomEventPayload<T>) => void,
  ): void
  off<T extends CustomEventName>(
    event: T,
    cb: (payload: InferCustomEventPayload<T>) => void,
  ): void
  send<T extends CustomEventName>(
    event: T,
    data?: InferCustomEventPayload<T>,
  ): void
}

必需的条件守卫

首先,确保所有 HMR API 的使用都放在条件块内,以便在生产环境中进行 tree-shaking:

js 复制代码
if (import.meta.hot) {
  // HMR 代码
}

TypeScript 的 IntelliSense

Vite 在 vite/client.d.ts 中提供了 import.meta.hot 的类型定义。你可以在 tsconfig.json 中添加 "vite/client",让 TypeScript 识别类型定义:

json [tsconfig.json] 复制代码
{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

hot.accept(cb)

模块要自我接受更新,可以使用带回调的 import.meta.hot.accept,回调会接收更新后的模块:

js twoslash 复制代码
import 'vite/client'
// ---cut---
export const count = 1

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    if (newModule) {
      // 当发生 SyntaxError 时 newModule 为 undefined
      console.log('updated: count is now ', newModule.count)
    }
  })
}

接受热更新的模块被视为 HMR 边界

dot 复制代码
digraph hmr_boundary {
  rankdir=RL
  ranksep=0.3
  node [shape=box style="rounded,filled" fontname="Arial" fontsize=11 margin="0.2,0.1" fontcolor="${#3c3c43|#ffffff}" color="${#c2c2c4|#3c3f44}"]
  edge [color="${#67676c|#98989f}" fontname="Arial" fontsize=10 fontcolor="${#67676c|#98989f}"]
  bgcolor="transparent"

  root [label="main.js" fillcolor="${#f6f6f7|#2e2e32}"]
  parent [label="App.vue" fillcolor="${#f6f6f7|#2e2e32}"]
  boundary [label="Component.vue\n(HMR 边界)\nhot.accept()" fillcolor="${#def5ed|#15312d}" color="${#18794e|#3dd68c}" penwidth=2]
  edited [label="utils.js\n(已编辑)" fillcolor="${#fcf4dc|#38301a}" color="${#915930|#f9b44e}" penwidth=2]

  boundary -> edited [label="导入" color="${#915930|#f9b44e}" style=bold]
  parent -> boundary [label="导入" style=dashed]
  root -> parent [label="导入" style=dashed]
}

Vite 的 HMR 实际上并不会替换最初导入的模块:如果一个 HMR 边界模块从依赖项重新导出导入内容,那么它需要负责更新这些重新导出的模块(并且这些导出必须使用 let)。此外,边界模块之上的导入者不会收到变更通知。这种简化的 HMR 实现对于大多数开发场景已经足够,同时让我们能够跳过生成代理模块这一昂贵的工作。

Vite 要求此函数的调用在源代码中必须形如 import.meta.hot.accept((对空白敏感),模块才能接受更新。这是 Vite 为了启用模块 HMR 支持而进行静态分析的要求。

hot.accept(deps, cb)

一个模块也可以接受来自直接依赖项的更新,而不重新加载自身:

js twoslash 复制代码
// @filename: /foo.d.ts
export declare const foo: () => void

// @filename: /example.js
import 'vite/client'
// ---cut---
import { foo } from './foo.js'

foo()

if (import.meta.hot) {
  import.meta.hot.accept('./foo.js', (newFoo) => {
    // 回调接收更新后的 './foo.js' 模块
    newFoo?.foo()
  })

  // 也可以接受依赖模块数组:
  import.meta.hot.accept(
    ['./foo.js', './bar.js'],
    ([newFooModule, newBarModule]) => {
      // 回调接收一个数组,其中只有更新后的模块是
      // 非 null。如果更新未成功(例如语法错误),
      // 数组为空
    },
  )
}

hot.dispose(cb)

一个自我接受的模块,或一个期望被其他模块接受的模块,可以使用 hot.dispose 清理由其更新副本产生的任何持久副作用:

js twoslash 复制代码
import 'vite/client'
// ---cut---
function setupSideEffect() {}

setupSideEffect()

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // 清理副作用
  })
}

hot.prune(cb)

注册一个回调,当模块不再被页面导入时调用。与 hot.dispose 相比,如果源代码在更新时自行清理副作用,并且你只需要在模块从页面移除时进行清理,可以使用此方法。Vite 目前将其用于 .css 导入。

js twoslash 复制代码
import 'vite/client'
// ---cut---
function setupOrReuseSideEffect() {}

setupOrReuseSideEffect()

if (import.meta.hot) {
  import.meta.hot.prune((data) => {
    // 清理副作用
  })
}

hot.data

Vite 为每个模块路径创建一个 import.meta.hot.data 对象。在 HMR 期间,同一个模块的连续实例之间会持久保留该对象。模块执行期间或通过传递给 hot.disposedata 参数所做的修改,对模块的下一个实例可见。

当模块被移除时,其 hot.disposehot.prune 回调会接收当前数据对象。这些回调完成后,Vite 会清除数据。如果模块稍后再次被导入,它会收到一个新的空数据对象。

请注意,不支持对 data 本身重新赋值。相反,你应该修改 data 对象的属性,以便保留从其他处理程序添加的信息。

js twoslash 复制代码
import 'vite/client'
// ---cut---
// 正确
import.meta.hot.data.someValue = 'hello'

// 不支持
import.meta.hot.data = { someValue: 'hello' }

hot.decline()

这目前是一个空操作(noop),为了向后兼容而存在。如果未来有新的用途,这可能会改变。若要表示模块不可热更新,请使用 hot.invalidate()

hot.invalidate(message?: string)

一个自我接受的模块可能在运行时意识到它无法处理 HMR 更新,因此需要将更新强制传播给导入者。通过调用 import.meta.hot.invalidate(),HMR 服务器会使调用方的导入者失效,就像调用方不是自我接受的一样。这将在浏览器控制台和终端中记录一条消息。你可以传入一个消息来提供失效发生的上下文。

请注意,即使你计划紧接着调用 invalidate,也应始终先调用 import.meta.hot.accept,否则 HMR 客户端将不会监听自我接受模块的未来更改。为了清楚地表达你的意图,我们建议在 accept 回调中调用 invalidate,如下所示:

js twoslash 复制代码
import 'vite/client'
// ---cut---
import.meta.hot.accept((module) => {
  // 你可以使用新的模块实例来决定是否使其失效。
  if (cannotHandleUpdate(module)) {
    import.meta.hot.invalidate()
  }
})

hot.on(event, cb)

监听 HMR 事件。

以下 HMR 事件由 Vite 自动派发:

  • 'vite:beforeUpdate' 当更新即将应用时(例如模块将被替换)
  • 'vite:afterUpdate' 当更新刚刚应用后(例如模块已被替换)
  • 'vite:beforeFullReload' 当即将发生完整重载时
  • 'vite:beforePrune' 当不再需要的模块即将被移除时
  • 'vite:invalidate' 当模块通过 import.meta.hot.invalidate() 失效时
  • 'vite:error' 当发生错误时(例如语法错误)
  • 'vite:ws:disconnect' 当 WebSocket 连接丢失时
  • 'vite:ws:connect' 当 WebSocket 连接(重新)建立时

自定义 HMR 事件也可以从插件发送。更多细节请参阅 handleHotUpdate

hot.off(event, cb)

从事件监听器中移除回调。

hot.send(event, data)

将自定义事件发送回 Vite 的开发服务器。

如果在连接之前调用,数据将被缓冲,并在连接建立后发送。

请参阅 客户端-服务器通信 了解更多细节,包括 自定义事件类型 部分。

延伸阅读

如果你想了解如何更深入地使用 HMR API 以及它在底层是如何工作的,请查看以下资源:

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。