路由
在 Bun.serve 中使用靜態路徑、引數和萬用字元定義路由
使用 routes 屬性(靜態路徑、引數和萬用字元)向 Bun.serve() 新增路由,或使用 fetch 方法處理未匹配的請求。
Bun.serve() 的路由器基於 uWebSocket 的樹形結構方案建置。路由器還添加了 SIMD 加速的路由引數解碼和 JavaScriptCore 結構快取,以充分發揮現代硬體的效能潛力。
基本設定
Bun.serve({
routes: {
"/": () => new Response("首頁"),
"/api": () => Response.json({ success: true }),
"/users": async () => Response.json({ users: [] }),
},
fetch() {
return new Response("未匹配路由");
},
});
Bun.serve() 中的路由會接收一個 BunRequest(繼承自 Request),並傳回一個 Response 或 Promise<Response>。由於路由使用這些 Request 和 Response 型別,因此傳送和接收 HTTP 請求時可以更輕鬆地使用相同的程式碼。
// 簡化範例
interface BunRequest<T extends string> extends Request {
params: Record<T, string>;
readonly cookies: CookieMap;
}
非同步路由
非同步/等待
在路由處理函式中使用 async/await,以傳回一個 Promise<Response>。
import { sql, serve } from "bun";
serve({
port: 3001,
routes: {
"/api/version": async () => {
const [version] = await sql`SELECT version()`;
return Response.json(version);
},
},
});
Promise
你也可以直接從路由處理函式傳回一個 Promise<Response>。
import { sql, serve } from "bun";
serve({
routes: {
"/api/version": () => {
return new Promise(resolve => {
setTimeout(async () => {
const [version] = await sql`SELECT version()`;
resolve(Response.json(version));
}, 100);
});
},
},
});
路由優先順序
Bun 按具體程度從高到低匹配路由:
- 精確路由(
/users/all) - 引數路由(
/users/:id) - 萬用字元路由(
/users/*) - 全域性捕獲(
/*)
Bun.serve({
routes: {
// 越具體的路由優先匹配
"/api/users/me": () => new Response("當前使用者"),
"/api/users/:id": req => new Response(`使用者 ${req.params.id}`),
"/api/*": () => new Response("API 捕獲全部"),
"/*": () => new Response("全域性捕獲全部"),
},
});
型別安全的路由引數
TypeScript 會解析以字串字面量形式傳入的路由引數,因此訪問 request.params 時,編輯器會顯示自動補全。
import type { BunRequest } from "bun";
Bun.serve({
routes: {
// 當以字串字面量傳入時,TypeScript 知道 params 的結構
"/orgs/:orgId/repos/:repoId": req => {
const { orgId, repoId } = req.params;
return Response.json({ orgId, repoId });
},
"/orgs/:orgId/repos/:repoId/settings": (
// 可選:可以顯式為 BunRequest 傳入型別引數:
req: BunRequest<"/orgs/:orgId/repos/:repoId/settings">,
) => {
const { orgId, repoId } = req.params;
return Response.json({ orgId, repoId });
},
},
});
Bun 會自動解碼百分號編碼的路由引數值,包括 Unicode 字元。對於無效的 Unicode,Bun 會將其替換為 Unicode 替換字元(\uFFFD)。
靜態響應
路由也可以是 Response 物件(無需處理函式)。Bun.serve() 會針對零分配排程對其進行最佳化,非常適合健康檢查、重定向和固定內容:
Bun.serve({
routes: {
// 健康檢查
"/health": new Response("OK"),
"/ready": new Response("準備就緒", {
headers: {
// 傳遞自定義頭部
"X-Ready": "1",
},
}),
// 重定向
"/blog": Response.redirect("https://bun.sh/blog"),
// API 響應
"/api/config": Response.json({
version: "1.0.0",
env: "production",
}),
},
});
靜態響應在初始化後不分配額外記憶體。相比手動傳回 Response 物件,通常能提升至少 15% 的效能。
Bun 會在伺服器物件的整個生命週期內快取靜態路由響應。要重新載入靜態路由,請呼叫 server.reload(options)。
檔案響應與靜態響應的區別
從路由提供檔案時,具體行為取決於你是將檔案內容緩衝起來,還是直接提供檔案:
Bun.serve({
routes: {
// 靜態路由 — 內容在啟動時緩衝到記憶體
"/logo.png": new Response(await Bun.file("./logo.png").bytes()),
// 檔案路由 — 每次請求從檔案系統讀取內容
"/download.zip": new Response(Bun.file("./download.zip")),
},
});
靜態路由 (new Response(await file.bytes())) 會在啟動時將內容緩衝至記憶體:
- 請求時無檔案系統 I/O — 內容完全從記憶體服務
- 支援 ETag — 自動生成與驗證快取的 ETag
- 支援 If-None-Match — 客戶端 ETag 匹配時傳回
304 Not Modified - 無 404 處理 — 缺失檔案在啟動時報錯,不在執行環境 404
- 記憶體佔用 — 檔案內容全量儲存於 RAM
- 適合用於:小型靜態資源、API 響應、頻繁訪問的檔案
檔案路由 (new Response(Bun.file(path))) 每次請求從檔案系統讀取:
- 每次請求都會執行檔案系統讀取,檢查檔案存在並讀取內容
- 內建 404 處理 — 若檔案不存在或變得不可訪問傳回
404 Not Found - 支援 Last-Modified — 基於檔案修改時間處理
If-Modified-Since頭 - 支援 If-Modified-Since — 快取有效時傳回
304 Not Modified - 支援 範圍請求 — 自動處理部分內容請求,傳回
Content-Range頭 - 支援流傳輸 — 使用帶背壓處理的緩衝讀取器,實現高效記憶體使用
- 記憶體高效 — 只緩衝傳輸時的小塊資料,不快取整個檔案
- 適合用於:大檔案、動態內容、使用者上傳、頻繁變更的檔案
目錄路由
若要在某個 URL 字首下提供整個目錄樹,請將 { dir } 作為路由值傳入。路由路徑必須以 /* 結尾。
Bun.serve({
routes: {
"/static/*": { dir: "./public" },
},
});
Bun 會對請求 URL 中字首之後的部分進行一次百分號解碼,並相對於 dir 開啟該路徑。對於非規範路徑,Bun 會傳回 404,因此實際提供的路徑始終是路由器所匹配的路徑。如果路徑中包含 .、..、空片段、%2F,或將可直接出現在路徑片段中的字元編碼為 %XX 序列,則該路徑為非規範路徑。在 Linux 上,開啟檔案時會使用 openat2(RESOLVE_IN_ROOT),因此核心會限制符號連結,防止其指向 dir 之外。
目錄路由與檔案路由共享響應處理路徑:
- Content-Type 根據副檔名設定。
- 每個響應都會發送 Last-Modified 和弱 ETag(
W/"<size>-<mtime>"),並會根據If-Modified-Since/If-None-Match傳回304 Not Modified。 - 支援 範圍請求,並使用
Accept-Ranges: bytes和Content-Range。 - 請求解析到的目錄若沒有尾隨
/,則會收到重定向到尾隨斜槓 URL 的301響應。若 URL 有尾隨斜槓,Bun 會從該目錄提供index.html。 - 缺失檔案會傳回
404。
傳入 statCache: false 可停用按路徑快取的 Last-Modified(每個路由可節省約 20 KB)。
流式傳輸檔案
要流式傳輸檔案,傳回一個以 BunFile 物件作為 body 的 Response。
Bun.serve({
fetch(req) {
return new Response(Bun.file("./hello.txt"));
},
});
要傳送檔案的一部分,請在 Bun.file 物件上使用 slice(start, end) 方法。Bun 會自動在 Response 物件上設定 Content-Range 和 Content-Length 標頭。
Bun.serve({
fetch(req) {
// 解析 `Range` 頭部
const [start = 0, end = Infinity] = req.headers
.get("Range") // Range: bytes=0-100
.split("=") // ["Range: bytes", "0-100"]
.at(-1) // "0-100"
.split("-") // ["0", "100"]
.map(Number); // [0, 100]
// 傳回檔案的片段
const bigFile = Bun.file("./big-video.mp4");
return new Response(bigFile.slice(start, end));
},
});
fetch 請求處理器
fetch 處理器會處理未匹配任何路由的傳入請求。它接收一個 Request 物件,並傳回一個 Response 或 Promise<Response>。
Bun.serve({
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") return new Response("首頁!");
if (url.pathname === "/blog") return new Response("部落格!");
return new Response("404!");
},
});
fetch 處理器支援 async/await:
import { sleep, serve } from "bun";
serve({
async fetch(req) {
const start = performance.now();
await sleep(10);
const end = performance.now();
return new Response(`睡眠了 ${end - start} 毫秒`);
},
});
也支援基於 Promise 的響應:
Bun.serve({
fetch(req) {
// 將請求轉發到另一個伺服器
return fetch("https://example.com");
},
});
fetch 處理器還會將 Server 物件作為其第二個引數接收。
// `server` 作為第二個引數傳遞給 `fetch`。
const server = Bun.serve({
fetch(req, server) {
const ip = server.requestIP(req);
return new Response(`您的 IP 是 ${ip.address}`);
},
});