热重载
Bun 开发服务器的热模块替换(HMR)
热模块替换(HMR)可以在不重新加载整个页面的情况下更新运行中应用程序的模块,同时保留应用程序状态。
import.meta.hot API 参考
Bun 实现了一个客户端 HMR API,其设计参考了 Vite 的 import.meta.hot API。你可以通过 if (import.meta.hot) 检查它,在生产环境中该代码会被摇树优化移除。
if (import.meta.hot) {
// HMR API 可用。
}
通常不需要进行此检查,因为 Bun 会在生产构建中消除对所有 HMR API 的调用。
// 整个函数调用都会在生产环境中被移除。
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
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 中相同。它不会修补此模块的导入者,而是使用新模块调用回调。
export const count = 0;
import.meta.hot.accept(newModule => {
if (newModule) {
// 当出现语法错误时 newModule 会是 undefined
console.log("updated: count is now ", newModule.count);
}
});
接受其它模块
import { count } from "./foo";
import.meta.hot.accept("./foo", newModule => {
if (!newModule) return;
console.log("updated: count is now ", count);
});
表示可以接受某个依赖项的模块。依赖项更新时,Bun 会使用新模块调用回调。
多依赖
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())。
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())
const sideEffect = setupSideEffect();
import.meta.hot.dispose(() => {
sideEffect.cleanup();
});
返回一个 promise 会延迟模块替换,直到模块完成卸载。Bun 会并行调用所有卸载回调。
import.meta.hot.prune()
添加一个清理回调。当该模块的所有导入都被移除,但模块之前已加载时,Bun 会调用它。
可以使用它来清理模块加载时创建的资源。与 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("热更新前");
});
替换文件时,Bun 会自动移除该文件的所有事件监听器。
内置事件
| 事件 | 触发时机 |
|---|---|
bun:beforeUpdate | 热更新应用前。 |
bun:afterUpdate | 热更新应用后。 |
bun:beforeFullReload | 完整页面重新加载前。 |
bun:beforePrune | 清理回调调用前。 |
bun:invalidate | 使用 import.meta.hot.invalidate() 使模块失效时。 |
bun:error | 发生构建或运行时错误时。 |
bun:ws:disconnect | HMR WebSocket 连接断开时。这可能表示开发服务器已离线。 |
bun:ws:connect | HMR WebSocket 连接或重新连接时。 |