热重载与开发服务器
本教程共 34 篇 · 第 6 篇 · 更新于 2026-08-06
本节目标:
- 区分
bun --watch(硬重启)与bun --hot(软重载)两种模式- 用
bun --hot让 HTTP 服务在不重启进程的情况下刷新代码- 了解 Bun 全栈开发服务器的默认 HMR 行为
- 会用
import.meta.hot做基础的生命周期管理
写代码时,最影响心流的就是「改一行就要手动重启」。Bun 内置了两种自动重载机制,都不需要 nodemon 之类的外部工具。这一节把它们讲清楚,并说清什么场景该用哪一个。
6.1 两种重载:硬重启 vs 软重载
Bun 提供两种模式,关键区别在「改完文件后进程要不要重来」:
| 模式 | 命令 | 行为 | 适用 |
|---|---|---|---|
--watch | bun --watch file.ts | 硬重启:进程退出并重新启动,环境变量与 CLI 参数保持不变 | 测试、一次性脚本、需要干净状态重启的场景 |
--hot | bun --hot file.ts | 软重载:不清退进程,只更新内部模块缓存,全局状态保留 | 长时间运行的服务、HTTP 服务器、需要保活状态的场景 |
简单记忆:--watch 是「关了再开」,--hot 是「原地换血」。
6.2 --watch:硬重启
适合 bun test 或运行普通脚本:
bun --watch index.tsx
bun --watch test
在 --watch 模式下,Bun 会追踪所有被 import 的文件,一旦其中任何一个发生变化,就带着相同的 CLI 参数与环境变量重新启动进程。如果进程崩溃,--watch 还会尝试重启。
Bun 的文件监听用的是操作系统原生 API(如 Linux 的 inotify、macOS/BSD 的 kqueue),而不是靠轮询,所以在大型项目里也能保持低开销、高灵敏度。文档里提到它做了若干优化:提高文件描述符的 rlimit、静态分配路径缓冲区、复用文件描述符等。
Tip多个
bun build --watch并行时,某个实例清屏可能掩盖其他实例的错误。加--no-clear-screen(或环境变量BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=true)可避免清屏,配合concurrently这类工具更好用。
6.3 --hot:软重载(HMR)
对需要长期运行的服务,--hot 更有价值。它不会重启整个进程,而是检测到文件变化后更新内部模块缓存:
bun --hot server.ts
从入口文件出发,Bun 会登记所有被 import 的源文件(不含 node_modules 里的),并监听它们。文件变化时执行「软重载」:所有文件被重新求值,但全局状态(特别是 globalThis)保留。
declare global {
var count: number;
}
globalThis.count ??= 0;
console.log(`Reloaded ${globalThis.count} times`);
globalThis.count++;
setInterval(() => {}, 1000000); // 防止进程退出
每保存一次文件,globalThis.count 就加一——因为进程没重启,globalThis 上的值被保留了下来。这正是 --hot 与 --watch 最本质的差异:前者保活、后者归零。
Note
bun --hot是服务端热重载,对应 Node 生态里「改后端代码不重启进程」的体验。它和浏览器里的 HMR(改 React 组件页面不刷新)不是一回事。想要浏览器端热更新,有两条路:用 Bun 的全栈开发服务器(前端 HMR 默认开启,见 6.5),或者前端另起一个 Vite。
6.4 HTTP 服务器的热更新
当你用 Bun.serve() 跑 HTTP 服务时,--hot 的效果最明显:保存文件后,Bun 会不重启进程地替换 fetch 处理器,刷新速度几乎瞬时。
Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello world");
},
});
bun --hot run index.ts
软重载的实现上,Bun 会重置内部的 require 缓存与 ES 模块注册表、同步跑一次垃圾回收、从头重新转译并求值代码。官方文档坦言这一步目前不是增量编译——没改的文件也会被重新转译,属于起步版实现。
Note传统文件监听器(如
nodemon)的做法是「重启整个进程」,于是 HTTP 服务器和数据库连接等状态对象都会丢失,重启还要重新监听端口、重新建立连接。bun --hot的「原地换血」避免了这些开销,因此刷新特别快。代价是全局状态会累积(比如上面globalThis.count一直涨),这既是特性也是需要注意的点:如果你的代码依赖「每次运行都从零开始」,那--watch反而更合适。
下面给一个完整的可运行示例,直观感受 --hot 下的服务端刷新:
// server.ts
Bun.serve({
port: 3000,
fetch(req: Request) {
const url = new URL(req.url);
if (url.pathname === "/count") {
globalThis.count = (globalThis.count ?? 0) + 1;
return new Response(`hit ${globalThis.count} times`);
}
return new Response("edit me and save!");
},
});
console.log("server ready, try: bun --hot run server.ts");
运行 bun --hot run server.ts 后,反复请求 /count,计数会持续累加(因为进程没重启);而如果你改了 fetch 里的返回文案再保存,新逻辑会立即生效,且计数不会被清零。把同样的代码换成 bun --watch,每次保存都会把计数归零——这正是两种模式的区别所在。
6.5 全栈开发服务器的默认 HMR
如果你用的是 Bun 的全栈开发服务器(基于 Bun.serve 配合前端入口),HMR 默认开启。也就是说,前端模块的改动会触发浏览器侧的热模块替换,无需额外配置。要显式关闭,可在 Bun.serve 里设置 development: { hmr: false }。
这套客户端 HMR 基于 import.meta.hot API,设计上对齐 Vite 的 import.meta.hot,方便从 Vite 生态迁移。生产构建时,Bun 会做死代码消除,把 HMR 相关调用整段移除,因此不必担心它们污染线上代码。
6.6 import.meta.hot 基础用法
最常见的两个用法是 accept() 与 data:
import.meta.hot.accept(); // 声明本模块可被热替换
accept() 表示「这个模块更新时可以被就地替换」。当被 accept 的模块或其依赖保存后,更新会向上冒泡、重新求值,并自动 patch 它的导入方。
import.meta.hot.data 用于在「旧模块」与「新模块」之间搬运状态:
import { createRoot } from "react-dom/client";
import { App } from "./app";
const root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(<App />); // 复用同一个 root,不重建
dispose() 则用于在模块被替换前做清理(如关闭连接、移除监听):
const sideEffect = setupSideEffect();
import.meta.hot.dispose(() => {
sideEffect.cleanup();
});
Warning
import.meta.hot的 API 必须直接以完整短语调用,不能先赋值给变量再调用,否则 Bun 无法识别。例如const hot = import.meta.hot; hot.accept();是无效的;必须写import.meta.hot.accept();。data可以传给函数,但hot本身不能。hot.invalidate()与hot.send()在当前版本尚未实现。
此外还有 import.meta.hot.on() / off() 用于监听 HMR 运行时事件,事件名带 bun: 前缀(为兼容 Vite 也支持 vite: 前缀)。常用事件包括:bun:beforeUpdate(热更新应用前)、bun:afterUpdate(应用后)、bun:beforeFullReload(整页刷新前)、bun:error(构建或运行时出错)、bun:ws:connect / bun:ws:disconnect(HMR WebSocket 连接状态)。当某文件被替换时,它上面的事件监听会被自动移除,无需手动清理。
import.meta.hot.on("bun:beforeUpdate", () => {
console.log("about to hot-update");
});
6.7 小结与选型建议
- 跑测试、临时脚本、需要干净重启 → 用
bun --watch。 - 跑 HTTP 服务、需要保活全局状态、追求瞬时刷新 → 用
bun --hot。 - 全栈开发服务器默认开启前端 HMR,无需配置;想关用
development: { hmr: false }。 import.meta.hot用于精细化控制热替换生命周期,对齐 Vite 习惯,但调用形式有约束、部分方法未实现。- 记住
--hot不刷新浏览器页面,它只管服务端代码替换。
Tip一个实用组合:
bun --hot run index.ts起后端服务,前端用 Vite 起 HMR。两者各管一边,开发体验接近「全栈热更新」。若想更省心,直接用 Bun 全栈开发服务器,前端 HMR 已内置。