热模块替换 (HMR) 允许您在运行的应用程序中更新模块,而无需完全重新加载页面。这保留了应用程序状态并改善了开发体验。
NOTE
使用 Bun 的全栈开发服务器时,默认启用 HMR。import.meta.hot API 参考
Bun 实现了 modeled after Vite 的 import.meta.hot API 的客户端 HMR API。可以使用 if (import.meta.hot) 检查,在生产环境中进行 tree-shaking。
if (import.meta.hot) {
// HMR APIs 可用。
}但是,此检查通常不需要,因为 Bun 会在生产构建中死代码消除所有 HMR API 的调用。
// 整个函数调用将在生产环境中移除!
import.meta.hot.dispose(() => {
console.log("dispose");
});NOTE
HMR API 仍在进行中。某些功能缺失。HMR 可以在 `Bun.serve` 中通过将 development 选项设置为 `{ hmr: false }` 来禁用。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() 方法表示模块可以热替换。当不带参数调用时,表示可以通过重新评估文件来替换此模块。热更新后,此模块的导入器将自动修补。
// 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 的功能与 Vite 中的功能相同。它不会修补此模块的导入器,而是使用新模块调用回调。
export const count = 0;
import.meta.hot.accept(newModule => {
if (newModule) {
// 发生 SyntaxError 时 newModule 为 undefined
console.log("updated: count is now ", newModule.count);
}
});接受其他模块
import { count } from "./foo";
import.meta.hot.accept("./foo", () => {
if (!newModule) return;
console.log("updated: count is now ", count);
});表示可以接受依赖的模块。当依赖更新时,将使用新模块调用回调。
与多个依赖一起使用
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())。
import { createRoot } from "react-dom/client";
import { App } from "./app";
const root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(<App />); // 重用现有根在生产环境中,data 内联为 {},这意味着它不能用作状态持有者。
import.meta.hot.dispose()
附加 on-dispose 回调。这在以下情况下调用:
- 在模块被另一个副本替换之前(在加载下一个之前)
- 在模块分离后(移除此模块的所有导入,请参阅
import.meta.hot.prune())
const sideEffect = setupSideEffect();
import.meta.hot.dispose(() => {
sideEffect.cleanup();
});返回 promise 将延迟模块替换,直到模块被处理。所有 dispose 回调并行调用。
import.meta.hot.prune()
附加 on-prune 回调。当此模块的所有导入都被移除时调用,但模块之前已加载。
这可用于清理在模块加载时创建的资源。与 import.meta.hot.dispose() 不同,这与 accept 和 data 更好地配对以管理有状态资源。管理 WebSocket 的完整示例:
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 运行时的事件。事件名称以前缀开头,以便插件不会相互冲突。
import.meta.hot.on("bun:beforeUpdate", () => {
console.log("before a hot update");
});当文件替换时,其所有事件监听器将自动移除。
内置事件
| 事件 | 触发时机 |
|---|---|
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:*` 使用。