Skip to content

你可以通過使用 routes 屬性(用於靜態路徑、參數和通配符)或使用 fetch 方法處理未匹配的請求,來向 Bun.serve() 添加路由。

Bun.serve() 的路由構建在 uWebSocket 的 基於樹的方法 之上,添加了 SIMD 加速的路由參數解碼JavaScriptCore 結構緩存,以推動現代硬件允許的性能極限。

基本設置

ts
Bun.serve({
  routes: {
    "/": () => new Response("Home"),
    "/api": () => Response.json({ success: true }),
    "/users": async () => Response.json({ users: [] }),
  },
  fetch() {
    return new Response("Unmatched route");
  },
});

Bun.serve() 中的路由接收 BunRequest(擴展了 Request)並返回 ResponsePromise<Response>。這使得發送和接收 HTTP 請求使用相同的代碼更容易。

ts
// 簡化以便簡潔
interface BunRequest<T extends string> extends Request {
  params: Record<T, string>;
  readonly cookies: CookieMap;
}

異步路由

Async/await

你可以在路由處理程序中使用 async/await 來返回 Promise<Response>

ts
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>

ts
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);
      });
    },
  },
});

路由優先級

路由按特異性順序匹配:

  1. 精確路由(/users/all
  2. 參數路由(/users/:id
  3. 通配符路由(/users/*
  4. 全局捕獲所有(/*
ts
Bun.serve({
  routes: {
    // 最具體的在前
    "/api/users/me": () => new Response("Current user"),
    "/api/users/:id": req => new Response(`User ${req.params.id}`),
    "/api/*": () => new Response("API catch-all"),
    "/*": () => new Response("Global catch-all"),
  },
});

類型安全的路由參數

當作為字符串字面量傳遞時,TypeScript 會解析路由參數,因此你的編輯器在訪問 request.params 時會顯示自動完成。

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 });
    },
  },
});

百分比編碼的路由參數值會自動解碼。支持 Unicode 字符。無效的 unicode 會被替換為 unicode 替換字符 ``。

靜態響應

路由也可以是 Response 對象(不帶處理程序函數)。Bun.serve() 針對零分配調度進行了優化——非常適合健康檢查、重定向和固定內容:

ts
Bun.serve({
  routes: {
    // 健康檢查
    "/health": new Response("OK"),
    "/ready": new Response("Ready", {
      headers: {
        // 傳遞自定義頭
        "X-Ready": "1",
      },
    }),

    // 重定向
    "/blog": Response.redirect("https://bun.com/blog"),

    // API 響應
    "/api/config": Response.json({
      version: "1.0.0",
      env: "production",
    }),
  },
});

靜態響應在初始化後不會分配額外的內存。你通常可以期望至少 15% 的性能提升,相比手動返回 Response 對象。

靜態路由響應在服務器的生命周期內緩存。要重新加載靜態路由,調用 server.reload(options)

文件響應與靜態響應

在路由中提供文件時,根據你是緩沖文件內容還是直接提供,有兩種不同的行為:

ts
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 支持 - 自動生成和驗證 ETags 用於緩存
  • 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 頭的部分內容請求
  • 流式傳輸 - 使用帶有背壓處理的緩沖讀取器以實現高效的內存使用
  • 內存高效 - 僅在傳輸期間緩沖小塊,而不是整個文件
  • 最適合:大文件、動態內容、用戶上傳、頻繁更改的文件

流式傳輸文件

要流式傳輸文件,返回一個帶有 BunFile 對象作為主體的 Response 對象。

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

你可以使用 Bun.file 對象上的 slice(start, end) 方法發送文件的一部分。這會自動在 Response 對象上設置 Content-RangeContent-Length 頭。

ts
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 對象並返回 ResponsePromise<Response>

ts
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/") return new Response("Home page!");
    if (url.pathname === "/blog") return new Response("Blog!");
    return new Response("404!");
  },
});

fetch 處理程序支持 async/await:

ts
import { sleep, serve } from "bun";

serve({
  async fetch(req) {
    const start = performance.now();
    await sleep(10);
    const end = performance.now();
    return new Response(`Slept for ${end - start}ms`);
  },
});

也支持基於 Promise 的響應:

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

你也可以從 fetch 處理程序訪問 Server 對象。它是傳遞給 fetch 函數的第二個參數。

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

Bun學習網由www.bunjs.com.cn整理維護