> ## Documentation Index
> Fetch the complete documentation index at: https://bun.ll1025.cn/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-shake 掉。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
if (import.meta.hot) {
  // HMR API 可用。
}
```

这个检查通常是不必要的，因为 Bun 在生产构建中会对所有 HMR API 调用进行死代码消除。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 这个函数调用在生产中被完全移除。
import.meta.hot.dispose(() => {
  console.log("dispose");
});
```

<Warning>
  为了使其工作，Bun 强制这些 API 被直接调用，不能间接调用。这意味着以下用法不起作用：

  ```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
  // 无效：将 `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` 中禁用 HMR，将 development 选项设置为 `{ hmr: false }`。
</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()` 方法表示一个模块可以被热替换。不带参数调用时，意味着这个模块可以通过重新评估文件来替换。热更新后，Bun 自动修补模块的导入者。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// index.ts
import { getCount } from "./foo.ts";

console.log("计数为 ", 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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export const count = 0;

import.meta.hot.accept(newModule => {
  if (newModule) {
    // newModule 在发生 SyntaxError 时为 undefined
    console.log("已更新：计数现在为 ", newModule.count);
  }
});
```

<Tip>优先使用不带参数的 `import.meta.hot.accept()`；它通常使代码更易于理解。</Tip>

### 接受其他模块

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { count } from "./foo";

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

  console.log("已更新：计数现在为 ", count);
});
```

表示依赖的模块可以被接受。当依赖项被更新时，Bun 用新模块调用回调。

### 带多个依赖项

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import.meta.hot.accept(["./foo", "./bar"], newModules => {
  // newModules 是一个数组，每个元素对应于更新后的模块
  // 如果该模块存在语法错误则为 undefined
});
```

此变体接受一个依赖项数组。回调接收更新后的模块，任何有错误的模块为 `undefined`。

## import.meta.hot.data

`import.meta.hot.data` 在热替换过程中将状态从模块的旧版本传送到新版本。写入 `import.meta.hot.data` 也会将该模块标记为自接受（相当于调用 `import.meta.hot.accept()`）。

```tsx title="index.tsx" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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()

附加一个 dispose 回调。这在以下情况下被调用：

* 在模块被另一个副本替换之前（下一个加载之前）
* 在模块被分离之后（移除了对此模块的所有导入，见 `import.meta.hot.prune()`）

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const sideEffect = setupSideEffect();

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

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

返回一个 Promise 会延迟模块替换，直到模块被 dispose。所有 dispose 回调并行调用。

## import.meta.hot.prune()

附加一个 prune 回调。当对此模块的所有导入都被移除时调用，但该模块之前被加载过。

用于清理模块加载时创建的资源。与 `import.meta.hot.dispose()` 不同，它更适合与 `accept` 和 `data` 配合使用来管理有状态的资源。一个管理 WebSocket 的完整示例：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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>
