Bun

热重载

热模块替换(HMR)允许您在运行中的应用程序中更新模块,而无需进行 完整的页面重新加载。这可以保留应用程序状态并改善开发体验。 使用 Bun 的全栈开发服务器时,HMR 默认启用。

HMR 默认在 Bun 的全栈开发服务器中使用。

import.meta.hot API 参考

Bun 实现了一个客户端 HMR API,其模型类似于 Vite 的 import.meta.hot API。可以使用 if (import.meta.hot) 进行检查,在生产环境中进行 tree-shaking。

if (import.meta.hot) {
  // HMR APIs are available.
}

然而,**通常不需要此检查**,因为 Bun 会在生产构建中对所有 HMR API 调用进行死代码消除。 为此,Bun 强制这些 API 必须直接调用。这意味着以下方法无效:

// This entire function call will be removed in production!
import.meta.hot.dispose(() => {
  console.log("dispose");
});

为此,Bun 强制这些 API 在没有间接调用。这意味着以下方法不起作用

invalid-hmr-usage.ts
// INVALID: Assigning `hot` to a variable
const hot = import.meta.hot;
hot.accept();

// INVALID: Assigning `import.meta` to a variable
const meta = import.meta;
meta.hot.accept();
console.log(meta.hot.data);

// INVALID: Passing to a function
doSomething(import.meta.hot.dispose);

// OK: The full phrase "import.meta.hot.<API>" must be called directly:
import.meta.hot.accept();

// OK: `data` can be passed to functions:
doSomething(import.meta.hot.data);

注意 — HMR API 仍在开发中。某些功能缺失。可以通过将 Bun.serve 中的 development 选项设置为 { hmr: false } 来禁用 HMR。

方法备注
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()(并且没有 React Fast Refresh 或插件为您调用它)时,当文件更新时页面将重新加载, 并且控制台警告会显示哪些文件已失效。如果依赖于完整的页面重新加载更合理,则可以安全地忽略此警告。 并且控制台警告会显示哪些文件已失效。如果依赖于完整的页面重新加载更合理,则此警告可以安全地忽略。

带回调函数

当提供一个回调函数时,import.meta.hot.accept 将像 Vite 中一样运行。它不会修补此模块的导入者,而是将 使用新模块调用回调函数。

export const count = 0;

import.meta.hot.accept(newModule => {
  if (newModule) {
    // newModule is undefined when SyntaxError happened
    console.log("updated: count is now ", newModule.count);
  }
});

建议使用不带参数的 import.meta.hot.accept(),因为它通常能使您的代码更容易理解。

接受其他模块

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 is an array where each item corresponds to the updated module
  // or undefined if that module had a syntax error
});

指示可以接受多个依赖项的模块。此变体接受一个依赖项数组,回调函数将接收更新后的模块,任何有错误的模块将收到 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 />); // re-use an existing root

在生产环境中,data 会被内联为 {},这意味着它不能用作状态持有者。

上述模式推荐用于有状态模块,因为 Bun 知道在生产环境中可以将 {}.prop ??= value 最小化为 value

import.meta.hot.dispose()

附加一个在处理时调用的回调函数。它在以下情况下被调用:

  • 在模块被另一个副本替换之前(在加载下一个模块之前)
  • 在模块分离后(移除所有对该模块的导入,请参见 import.meta.hot.prune()
const sideEffect = setupSideEffect();

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

此回调函数在路由导航或浏览器选项卡关闭时不会被调用。

返回一个 Promise 将延迟模块替换,直到模块被处理。 所有处理回调并行调用。

import.meta.hot.prune()

附加一个在修剪时回调的函数。当所有对该模块的导入 都被移除,但该模块之前已加载时,此函数会被调用。

这可用于清理模块加载时创建的资源。 import.meta.hot.dispose() 不同,它与 acceptdata 更好地配合,用于管理有状态资源。一个管理 WebSocket 的完整示例

import { something } from "./something";

// Initialize or re-use a WebSocket connection
export const ws = (import.meta.hot.data.ws ??= new WebSocket(location.origin));

// If the module's import is removed, clean up the WebSocket connection.
import.meta.hot.prune(() => {
  ws.close();
});

如果使用 dispose,WebSocket 将在每次热更新时关闭并重新打开。 这两个版本的代码都将在导入文件更新时防止页面重新加载。 这两个版本的代码都将在导入文件更新时防止页面重新加载。

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

on()off() 用于监听 HMR 运行时发出的事件。事件名称带有前缀,以避免插件之间冲突。

import.meta.hot.on("bun:beforeUpdate", () => {
  console.log("before a hot update");
});

当文件被替换时,其所有事件监听器都会自动移除。

所有内置事件列表

Event发出时间
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 连接或重新连接时。

为了与 Vite 兼容,上述事件也可以通过 vite:* 前缀而不是 bun:* 使用。