執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

路由

在 Bun.serve 中使用靜態路徑、引數和萬用字元定義路由

使用 routes 屬性(靜態路徑、引數和萬用字元)向 Bun.serve() 新增路由,或使用 fetch 方法處理未匹配的請求。

Bun.serve() 的路由器基於 uWebSocket 的樹形結構方案建置。路由器還添加了 SIMD 加速的路由引數解碼和 JavaScriptCore 結構快取,以充分發揮現代硬體的效能潛力。

基本設定

server.ts
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 請求時可以更輕鬆地使用相同的程式碼。

TypeScript
// 簡化範例
interface BunRequest<T extends string> extends Request {
  params: Record<T, string>;
  readonly cookies: CookieMap;
}

非同步路由

非同步/等待

在路由處理函式中使用 async/await,以傳回一個 Promise<Response>。

TypeScript
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>。

TypeScript
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 按具體程度從高到低匹配路由:

  1. 精確路由(/users/all)
  2. 引數路由(/users/:id)
  3. 萬用字元路由(/users/*)
  4. 全域性捕獲(/*)
TypeScript
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 時,編輯器會顯示自動補全。

index.ts
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() 會針對零分配排程對其進行最佳化,非常適合健康檢查、重定向和固定內容:

TypeScript
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)。

檔案響應與靜態響應的區別

從路由提供檔案時,具體行為取決於你是將檔案內容緩衝起來,還是直接提供檔案:

TypeScript
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 } 作為路由值傳入。路由路徑必須以 /* 結尾。

TypeScript
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。

TypeScript
Bun.serve({
  fetch(req) {
    return new Response(Bun.file("./hello.txt"));
  },
});

要傳送檔案的一部分,請在 Bun.file 物件上使用 slice(start, end) 方法。Bun 會自動在 Response 物件上設定 Content-Range 和 Content-Length 標頭。

TypeScript
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>。

TypeScript
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:

TypeScript
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 的響應:

TypeScript
Bun.serve({
  fetch(req) {
    // 將請求轉發到另一個伺服器
    return fetch("https://example.com");
  },
});

fetch 處理器還會將 Server 物件作為其第二個引數接收。

TypeScript
// `server` 作為第二個引數傳遞給 `fetch`。
const server = Bun.serve({
  fetch(req, server) {
    const ip = server.requestIP(req);
    return new Response(`您的 IP 是 ${ip.address}`);
  },
});