熱過載
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 連線或重新連線時。 |