Skip to main content
热模块替换(HMR)允许你在正在运行的应用中更新模块,而无需完全刷新页面。这样可以保留应用状态,提升开发体验。
在使用 Bun 全栈开发服务器时,HMR 默认启用。

import.meta.hot API 参考

Bun 实现了一个客户端 HMR API,参考了 Vite 的 import.meta.hot API。可以用 if (import.meta.hot) 来检测,生产环境中会被 tree-shaking 去除。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
if (import.meta.hot) {
  // HMR API 可用。
}
不过,这种检测通常不需要,因为 Bun 会在生产构建时对所有 HMR API 调用进行死代码删除。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
// 这个函数调用在生产环境会被完全移除!
import.meta.hot.dispose(() => {
  console.log("dispose");
});
为了让这正常工作,Bun 强制要求这些 API 必须直接调用,不能通过间接引用。也就是说,以下用法是无效的:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
// 无效:将 `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);
HMR API 仍在开发中,一些功能尚未实现。在 Bun.serve 中可以通过设置 development 选项 { hmr: false } 来禁用 HMR。

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() 方法表示模块可以被热替换。无参数调用时,表示该模块可简单通过重新执行文件来替换。热更新后,该模块的所有导入者会自动被更新补丁。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.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 并重新执行。导入 index.ts 的文件随后会被更新补丁,导入新版本的 getNegativeCount()。如果只有 index.ts 被更新,只有该文件重新执行,且 foo.ts 里的计数器会重用。 这可以与 import.meta.hot.data 结合使用,将状态从旧模块传递到新模块。
当没有任何模块调用 import.meta.hot.accept()(也没有 React Fast Refresh 或插件调用它), 文件更新时页面会刷新,且控制台会显示哪些文件被失效。若你更倾向于使用页面刷新,这个警告可忽略。

带回调

如果传入一个回调,import.meta.hot.accept 将像 Vite 那样工作。它不会自动补丁导入者,而是把新模块作为参数调用回调。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
export const count = 0;

import.meta.hot.accept(newModule => {
  if (newModule) {
    // 当出现语法错误时 newModule 会是 undefined
    console.log("updated: count is now ", newModule.count);
  }
});
推荐使用无参数的 import.meta.hot.accept(),这通常让代码更易理解。

接受其它模块

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import { count } from "./foo";

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

  console.log("updated: count is now ", count);
});
表示可以接受某个依赖模块的热替换。当该依赖更新时,回调将被调用并传入新模块。

多依赖

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
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())。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.tsx
import { createRoot } from "react-dom/client";
import { App } from "./app";

const root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(<App />); // 复用已有 root
生产环境中,data 会被内联为 {},因此不能用作状态持有。
上述模式推荐用于有状态模块,因为 Bun 知道能在生产环境将 {}.prop ??= value 优化为 value

import.meta.hot.dispose()

绑定一个销毁回调。该回调在以下时被调用:
  • 模块即将被替换(即新模块加载前)
  • 模块被卸载(所有对该模块的导入被移除,见 import.meta.hot.prune()
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
const sideEffect = setupSideEffect();

import.meta.hot.dispose(() => {
  sideEffect.cleanup();
});
该回调不会在路由导航或浏览器标签关闭时调用。
返回 Promise 会延迟模块替换,直到 Promise 解决。所有 dispose 回调并行执行。

import.meta.hot.prune()

绑定一个清理回调。在所有导入这个模块的引用被移除之后调用,但该模块之前已经加载过。 这可以用来清理模块加载时创建的资源。相比于 import.meta.hot.dispose(),这个方法更适合与 acceptdata 配合管理有状态资源。以下是一个管理 WebSocket 的完整示例:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.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();
});
若使用 dispose,WebSocket 会在每次热更新时关闭并重新打开。两种写法都能避免导入文件更新时页面刷新。

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

on()off() 用于监听 HMR 运行时事件。事件名称带有前缀,避免插件间冲突。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
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:disconnectHMR WebSocket 连接断开时,表示开发服务器可能离线
bun:ws:connectHMR WebSocket 连接或重新连接时
为兼容 Vite,以上事件也可用带 vite:* 前缀替代 bun:*