运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

热重载

Bun 开发服务器的热模块替换(HMR)

热模块替换(HMR)可以在不重新加载整个页面的情况下更新运行中应用程序的模块,同时保留应用程序状态。

import.meta.hot API 参考

Bun 实现了一个客户端 HMR API,其设计参考了 Vite 的 import.meta.hot API。你可以通过 if (import.meta.hot) 检查它,在生产环境中该代码会被摇树优化移除。

index.ts
if (import.meta.hot) {
  // HMR API 可用。
}

通常不需要进行此检查,因为 Bun 会在生产构建中消除对所有 HMR API 的调用。

index.ts
// 整个函数调用都会在生产环境中被移除。
import.meta.hot.dispose(() => {
  console.log("dispose");
});

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 可以通过重新求值该文件来替换此模块。热更新后,Bun 会自动修补该模块的导入者。

index.ts
// 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,然后重新求值。接着,Bun 会修补导入 index.ts 的文件,使其导入新版 getNegativeCount()。如果你只更新 index.ts,Bun 只会重新求值该文件,并复用 foo.ts 中的计数器。

将此功能与 import.meta.hot.data 结合使用,可以将状态从之前的模块传递到新模块。

带回调

传入回调时,import.meta.hot.accept 的行为与在 Vite 中相同。它不会修补此模块的导入者,而是使用新模块调用回调。

index.ts
export const count = 0;

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

接受其它模块

index.ts
import { count } from "./foo";

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

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

表示可以接受某个依赖项的模块。依赖项更新时,Bun 会使用新模块调用回调。

多依赖

index.ts
import.meta.hot.accept(["./foo", "./bar"], newModules => {
  // newModules holds the updated module at its index and
  // undefined for the other dependencies
});

此变体接受一个依赖项数组。回调会收到一个数组,其中更新后的模块位于对应索引处,其它依赖项对应的位置为 undefined。

import.meta.hot.data

import.meta.hot.data 会在热替换过程中,将模块先前版本的状态传递给新版本。向 import.meta.hot.data 写入内容也会将模块标记为自行接受(等同于调用 import.meta.hot.accept())。

index.tsx
import { createRoot } from "react-dom/client";
import { App } from "./app";

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

在生产环境中,Bun 会将 data 内联为 {},因此不能将其用作状态持有器。

import.meta.hot.dispose()

添加一个卸载回调。Bun 会在以下情况调用它:

  • 模块即将被替换(即新模块加载前)
  • 模块被卸载(所有对该模块的导入被移除,见 import.meta.hot.prune())
index.ts
const sideEffect = setupSideEffect();

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

返回一个 promise 会延迟模块替换,直到模块完成卸载。Bun 会并行调用所有卸载回调。

import.meta.hot.prune()

添加一个清理回调。当该模块的所有导入都被移除,但模块之前已加载时,Bun 会调用它。

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

index.ts
import { something } from "./something";

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

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

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

使用 on() 和 off() 监听来自 HMR 运行时的事件。事件名称带有前缀,因此插件之间不会发生冲突。

index.ts
import.meta.hot.on("bun:beforeUpdate", () => {
  console.log("热更新前");
});

替换文件时,Bun 会自动移除该文件的所有事件监听器。

内置事件

事件触发时机
bun:beforeUpdate热更新应用前。
bun:afterUpdate热更新应用后。
bun:beforeFullReload完整页面重新加载前。
bun:beforePrune清理回调调用前。
bun:invalidate使用 import.meta.hot.invalidate() 使模块失效时。
bun:error发生构建或运行时错误时。
bun:ws:disconnectHMR WebSocket 连接断开时。这可能表示开发服务器已离线。
bun:ws:connectHMR WebSocket 连接或重新连接时。