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整理维护