> ## Documentation Index
> Fetch the complete documentation index at: https://bun.zhcndoc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 热重载

> Bun 开发服务器的热模块替换（HMR）

热模块替换（HMR）允许你在正在运行的应用中更新模块，而无需完全刷新页面。这样可以保留应用状态，提升开发体验。

<Note>在使用 Bun 全栈开发服务器时，HMR 默认启用。</Note>

## `import.meta.hot` API 参考

Bun 实现了一个客户端 HMR API，参考了 [Vite 的 `import.meta.hot` API](https://vite.dev/guide/api-hmr)。可以用 `if (import.meta.hot)` 来检测，生产环境中会被 tree-shaking 去除。

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
if (import.meta.hot) {
  // HMR API 可用。
}
```

不过，这种检测通常不需要，因为 Bun 会在生产构建时对所有 HMR API 调用进行死代码删除。

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
// 这个函数调用在生产环境会被完全移除！
import.meta.hot.dispose(() => {
  console.log("dispose");
});
```

<Warning>
  为了让这正常工作，Bun 强制要求这些 API 必须直接调用，不能通过间接引用。也就是说，以下用法是无效的：

  ```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
  // 无效：将 `hot` 赋值给变量
  const hot = import.meta.hot;
  hot.accept();

  // 无效：将 `import.meta` 赋值给变量
  const meta = import.meta;
  meta.hot.accept();
  console.log(meta.hot.data);

  // 无效：作为参数传递
  doSomething(import.meta.hot.dispose);

  // 有效：必须直接调用完整表达式 "import.meta.hot.<API>"：
  import.meta.hot.accept();

  // 有效：`data` 可以作为参数传递：
  doSomething(import.meta.hot.data);
  ```
</Warning>

<Note>
  HMR API 仍在开发中，一些功能尚未实现。在 `Bun.serve` 中可以通过设置 development 选项 `{ hmr: false }` 来禁用 HMR。
</Note>

## API 方法

| 方法                 | 状态 | 说明                                 |
| ------------------ | -- | ---------------------------------- |
| `hot.accept()`     | ✅  | 表示模块支持热更新可以安全替换。                   |
| `hot.data`         | ✅  | 在模块更新间保持数据状态。                      |
| `hot.dispose()`    | ✅  | 添加模块即将替换时执行的回调函数。                  |
| `hot.invalidate()` | ❌  |                                    |
| `hot.on()`         | ✅  | 绑定事件监听器。                           |
| `hot.off()`        | ✅  | 移除通过 `on` 绑定的事件监听器。                |
| `hot.send()`       | ❌  |                                    |
| `hot.prune()`      | 🚧 | 注意：回调目前从未被调用。                      |
| `hot.decline()`    | ✅  | 无操作函数，匹配 Vite 的 `import.meta.hot`。 |

## import.meta.hot.accept()

`accept()` 方法指示模块可以进行热替换。当不传入参数调用时，表示该模块可以通过重新评估文件来替换。在热更新之后，对该模块的导入者将会被自动修补。

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
// index.ts
import { getCount } from "./foo.ts";

console.log("count is ", getCount());

import.meta.hot.accept();

export function getNegativeCount() {
  return -getCount();
}
```

这为 `index.ts` 引入的所有文件创建了一个热重载边界。也就是说，只要 `foo.ts` 或其任何依赖被保存修改，更新会冒泡到 `index.ts` 并重新执行。随后，导入 `index.ts` 的文件会被更新补丁，导入新的版本的 `getNegativeCount()`。如果只有 `index.ts` 被更新，只有该文件会重新执行，并且 `foo.ts` 里的计数器会重用。

这可以与 `import.meta.hot.data` 结合使用，将状态从旧模块传递到新模块。

<Info>
  当没有任何模块调用 `import.meta.hot.accept()`（也没有 React Fast Refresh 或插件调用它），
  文件更新时页面会刷新，并且控制台会显示哪些文件被失效。若你更倾向于使用页面刷新，这个警告可忽略。
</Info>

### 带回调

如果传入一个回调，`import.meta.hot.accept` 将像 Vite 那样工作。它不会自动补丁导入者，而是把新模块作为参数调用回调。

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
export const count = 0;

import.meta.hot.accept(newModule => {
  if (newModule) {
    // 当出现语法错误时 newModule 会是 undefined
    console.log("updated: count is now ", newModule.count);
  }
});
```

<Tip>
  推荐使用无参数的 `import.meta.hot.accept()`，这通常让代码更易理解。
</Tip>

### 接受其它模块

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { count } from "./foo";

import.meta.hot.accept("./foo", () => {
  if (!newModule) return;

  console.log("updated: count is now ", count);
});
```

表示可以接受某个依赖模块的热替换。当该依赖更新时，回调将被调用并传入新模块。

### 多依赖

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import.meta.hot.accept(["./foo", "./bar"], newModules => {
  // newModules 是数组，每项对应一个更新的模块
  // 或者如果模块语法有错误则对应 undefined
});
```

表示可以接受多个依赖模块。该重载版本接受依赖数组，回调收到的是对应的更新模块数组，对于有错误的模块是 `undefined`。

## import.meta.hot.data

`import.meta.hot.data` 用于在热替换期间在模块实例间持久化状态，从前一个模块传递数据到新模块。当写入 `import.meta.hot.data` 时，Bun 会同时标记该模块为自接受（相当于调用了 `import.meta.hot.accept()`）。

```tsx title="index.tsx" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { createRoot } from "react-dom/client";
import { App } from "./app";

const root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(<App />); // 复用已有 root
```

在生产环境中，`data` 会被内联为 `{}`，因此不能用作状态持有。

<Tip>
  上述模式推荐用于有状态模块，因为 Bun 知道能在生产环境将 `{}.prop ??= value` 优化为 `value`。
</Tip>

## import.meta.hot.dispose()

绑定一个销毁回调。该回调在以下时机被调用：

* 模块即将被替换（即新模块加载前）
* 模块被卸载（所有对该模块的导入被移除，见 `import.meta.hot.prune()`）

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
const sideEffect = setupSideEffect();

import.meta.hot.dispose(() => {
  sideEffect.cleanup();
});
```

<Warning>该回调不会在路由导航或浏览器标签关闭时调用。</Warning>

返回 Promise 会延迟模块替换，直到 Promise 解决。所有 dispose 回调并行执行。

## import.meta.hot.prune()

绑定一个清理回调。在所有导入这个模块的引用被移除之后调用，但该模块之前已经加载过。

这可以用来清理模块加载时创建的资源。相比于 `import.meta.hot.dispose()`，这个方法更适合与 `accept` 和 `data` 配合管理有状态资源。以下是一个管理 WebSocket 的完整示例：

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { something } from "./something";

// 初始化或复用 WebSocket 连接
export const ws = (import.meta.hot.data.ws ??= new WebSocket(location.origin));

// 模块的所有导入被移除时，清理 WebSocket 连接
import.meta.hot.prune(() => {
  ws.close();
});
```

<Info>
  若使用 `dispose`，WebSocket 会在每次热更新时关闭并重新打开。两种写法都能避免导入文件更新时页面刷新。
</Info>

## import.meta.hot.on() 和 off()

`on()` 和 `off()` 用于监听 HMR 运行时事件。事件名称带有前缀，避免插件间冲突。

```ts title="index.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import.meta.hot.on("bun:beforeUpdate", () => {
  console.log("热更新前");
});
```

当文件被替换时，所有相关事件监听器会自动移除。

### 内置事件

| 事件                     | 触发时机                                      |
| ---------------------- | ----------------------------------------- |
| `bun:beforeUpdate`     | 在热更新应用之前                                  |
| `bun:afterUpdate`      | 在热更新应用之后                                  |
| `bun:beforeFullReload` | 在页面完全刷新前                                  |
| `bun:beforePrune`      | 在调用 prune 回调前                             |
| `bun:invalidate`       | 当调用 `import.meta.hot.invalidate()` 使模块失效时 |
| `bun:error`            | 当构建或运行时发生错误                               |
| `bun:ws:disconnect`    | HMR WebSocket 连接断开时，表示开发服务器可能离线           |
| `bun:ws:connect`       | HMR WebSocket 连接或重新连接时                    |

<Note>为兼容 Vite，以上事件也可用带 `vite:*` 前缀替代 `bun:*`。</Note>
