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

伺服器

使用 Bun.serve 在 Bun 中啟動高效能 HTTP 伺服器

基本設定

index.ts
const server = Bun.serve({
  // `routes` 需要 Bun v1.2.3+
  routes: {
    // 靜態路由
    "/api/status": new Response("OK"),

    // 動態路由
    "/users/:id": req => {
      return new Response(`你好,使用者 ${req.params.id}!`);
    },

    // 按 HTTP 方法處理
    "/api/posts": {
      GET: () => new Response("帖子列表"),
      POST: async req => {
        const body = await req.json();
        return Response.json({ created: true, ...body });
      },
    },

    // 萬用字元路由,匹配所有以 "/api/" 開頭且未被其他路由匹配的請求
    "/api/*": Response.json({ message: "未找到" }, { status: 404 }),

    // 從 /blog/hello 重定向到 /blog/hello/world
    "/blog/hello": Response.redirect("/blog/hello/world"),

    // 懶載入檔案並提供服務
    "/favicon.ico": Bun.file("./favicon.ico"),
  },

  // (可選)未匹配路由的回退處理:
  // 如果 Bun 版本低於 1.2.3,則需要
  fetch(req) {
    return new Response("未找到", { status: 404 });
  },
});

console.log(`伺服器正在運行於 ${server.url}`);

HTML 匯入

直接將 HTML 檔案匯入伺服器程式碼,以建置同時包含伺服器端和客戶端程式碼的全棧應用。HTML 匯入支援兩種模式:

開發環境(bun --hot): Bun 會在執行環境按需打包資源,並啟用熱模組替換(HMR):當你修改前端程式碼時,瀏覽器會更新,而無需完整重新載入頁面。

生產環境(bun build): 使用 bun build --target=bun 建置時,import index from "./index.html" 語句會解析為一個預建置的清單物件,其中包含所有已打包的客戶端資源。Bun.serve 會從此清單提供資源,無需在執行環境進行打包。

TypeScript
import myReactSinglePageApp from "./index.html";

Bun.serve({
  routes: {
    "/": myReactSinglePageApp,
  },
});

HTML 匯入的功能不止是提供 HTML:它們還會執行 Bun 的 打包器、JavaScript 轉譯器和 CSS 解析器,因此你可以使用 React、TypeScript 和 Tailwind CSS 建置前端。

如需瞭解使用 HTML 匯入建置全棧應用的完整指南,請參閱全棧開發伺服器。


設定

更改 port 和 hostname

要設定伺服器監聽的埠和主機名,請在選項物件中設定 port 和 hostname。

TypeScript
Bun.serve({
  port: 8080, // 預設使用 $BUN_PORT、$PORT、$NODE_PORT,否則為 3000 // [!code ++]
  hostname: "mydomain.com", // 預設使用 "0.0.0.0" // [!code ++]
  fetch(req) {
    return new Response("404!");
  },
});

設定 port 為 0 來隨機選擇一個可用埠。

TypeScript
const server = Bun.serve({
  port: 0, // 隨機埠 // [!code ++]
  fetch(req) {
    return new Response("404!");
  },
});

// server.port 是隨機選擇的埠
console.log(server.port);

從伺服器的 port 或 url 屬性中讀取所選埠。

TypeScript
console.log(server.port); // 3000
console.log(server.url); // http://localhost:3000

設定預設埠

有多個標誌和環境變數可以設定預設埠,當未設定 port 選項時,Bun 會使用該埠。

  • --port 命令列引數
shell
bun --port=4002 server.ts
  • BUN_PORT 環境變數
shell
BUN_PORT=4002 bun server.ts
  • PORT 環境變數
terminal
PORT=4002 bun server.ts
  • NODE_PORT 環境變數
terminal
NODE_PORT=4002 bun server.ts

Unix 域套接字

要監聽 Unix 域套接字,請傳入 unix 選項並指定套接字路徑。

TypeScript
Bun.serve({
  unix: "/tmp/my-socket.sock", // 套接字路徑
  fetch(req) {
    return new Response(`404!`);
  },
});

抽象名稱空間套接字

在 Linux 上,Bun 還支援抽象名稱空間套接字:在 unix 路徑前加上一個空位元組。

TypeScript
Bun.serve({
  unix: "\0my-abstract-socket", // 抽象名稱空間套接字
  fetch(req) {
    return new Response(`404!`);
  },
});

與 Unix 域套接字不同,抽象名稱空間套接字不繫結到檔案系統,並且在最後一個引用關閉時會自動刪除。


HTTP/3 (QUIC)

Bun.serve 也可以通過 QUIC 監聽 HTTP/3。將 http3: true 與 tls 一起設定;HTTP/3 需要 TLS。

TypeScript
Bun.serve({
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  http3: true, // [!code ++]
  fetch(req) {
    return new Response("通過 HTTP/3 你好!");
  },
});

啟用 http3 後,伺服器會在同一埠上同時通過 TCP(HTTP/1.1)和 UDP(HTTP/3)監聽。HTTP/1.1 響應會包含一個 Alt-Svc 標頭,宣告 HTTP/3 端點,以便支援的客戶端可以自動升級。

若要僅提供 HTTP/3 服務而完全不啟用 TCP 監聽,請設定 http1: false:

TypeScript
Bun.serve({
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  http3: true,
  http1: false, // [!code ++]
  fetch(req) {
    return new Response("僅 HTTP/3");
  },
});

HTTP/2

設定 http2: true,即可在同一埠上使用與 HTTP/1.1 相同的路由和 fetch 處理程序提供 HTTP/2 服務。

TypeScript
Bun.serve({
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  http2: true, // [!code ++]
  fetch(req) {
    return new Response("通過 HTTP/2 你好!");
  },
});

使用 tls 時,Bun 會通過 ALPN 為每個連線選擇協議。提供 h2 的客戶端(瀏覽器、curl、node:http2)會使用 HTTP/2。其他客戶端則使用 HTTP/1.1。

不使用 tls 時,Bun 會為使用 HTTP/2 前言(“先驗知識”)建立連線的客戶端提供 HTTP/2 服務,例如 curl --http2-prior-knowledge 或來自 node:http2 的 http2.connect("http://...")。其他連線則使用 HTTP/1.1。

設定 http1: false 可拒絕 HTTP/1.x 客戶端。使用 TLS 時,提供 ALPN 但不包含 h2 的客戶端會在握手時失敗;完全不傳送 ALPN 的客戶端,以及未以該前言開頭的明文連線,會收到 505 HTTP Version Not Supported。設定 http1: false 後,伺服器無法接受 WebSocket 連線,因為 server.upgrade() 僅適用於 HTTP/1.1。

server.upgrade() 僅適用於 HTTP/1.1 請求。不支援 HTTP/2 上的 WebSocket、伺服器推送和響應尾部標頭(gRPC 所需)。


idleTimeout

預設情況下,Bun.serve 會在連線空閒 10 秒後將其關閉。沒有資料正在傳送或接收時,連線即為空閒。這也包括正在處理中的請求:處理程序仍在執行,但尚未向響應寫入任何位元組。瀏覽器和 fetch() 客戶端會將其視為連線重置。

要進行設定,請設定 idleTimeout 欄位(單位為秒)。最大值為 255,設定為 0 則完全停用超時。

TypeScript
Bun.serve({
  // 30 秒(預設值為 10)
  idleTimeout: 30,

  fetch(req) {
    return new Response("Bun!");
  },
});

export default 語法

無需將伺服器選項傳入 Bun.serve,你可以直接 export default 這些選項。

server.ts
import type { Serve } from "bun";

export default {
  fetch(req) {
    return new Response("Bun!");
  },
} satisfies Serve.Options<undefined>;

型別引數 <undefined> 是 WebSocket 資料型別。如果你添加了一個 WebSocket 處理程序,並通過 server.upgrade(req, { data: ... }) 附加自定義資料,請將 undefined 替換為你的資料型別。

可以直接執行此檔案:當 Bun 檢測到某個檔案的 default 匯出中包含 fetch 處理程序時,就會將該匯出傳入 Bun.serve。


熱路由過載

使用 server.reload() 無需重啟伺服器即可更新路由:

TypeScript
const server = Bun.serve({
  routes: {
    "/api/version": () => Response.json({ version: "1.0.0" }),
  },
});

// 部署新路由無停機
server.reload({
  routes: {
    "/api/version": () => Response.json({ version: "2.0.0" }),
  },
});

伺服器生命週期方法

server.stop()

停止伺服器接受新連線:

TypeScript
const server = Bun.serve({
  fetch(req) {
    return new Response("Hello!");
  },
});

// 優雅停止伺服器(等待正在進行的請求完成)
await server.stop();

// 強制停止並關閉所有活躍連線
await server.stop(true);

預設情況下,stop() 會允許正在處理的請求和 WebSocket 連線完成。伺服器會立即關閉空閒的 keep-alive 連線。正在處理請求的連線會在伺服器傳送響應後關閉。傳入 true 則會立即終止所有連線。每當所有連線都關閉後,傳回的 Promise 就會解析。

server.closeIdleConnections()

在不停止伺服器的情況下,關閉當前未處理請求的 keep-alive 連線:

TypeScript
const closed = server.closeIdleConnections();
console.log(`closed ${closed} idle connections`);

它會傳回已關閉的連線數量。正在處理請求的連線和已開啟的 WebSocket 不受影響,伺服器仍會接受新連線。此方法與 node:http 的 server.closeIdleConnections() 行為一致,後者不傳回任何內容。

server.ref() 和 server.unref()

控制伺服器是否保持 Bun 行程存活:

TypeScript
// 如果伺服器是唯一執行的東西,不阻止行程退出
server.unref();

// 恢復預設行為 - 保持行程存活
server.ref();

server.reload()

無需重啟即可更新伺服器處理器:

TypeScript
const server = Bun.serve({
  routes: {
    "/api/version": Response.json({ version: "v1" }),
  },
  fetch(req) {
    return new Response("v1");
  },
});

// 更新為新處理器
server.reload({
  routes: {
    "/api/version": Response.json({ version: "v2" }),
  },
  fetch(req) {
    return new Response("v2");
  },
});

可將此方法用於開發和熱過載。你只能更新 fetch、error、routes 和 websocket。


每請求控制

server.timeout(Request, seconds)

覆蓋單個請求的空閒超時時間。傳遞 0 可以完全停用該請求的超時時間。

TypeScript
const server = Bun.serve({
  async fetch(req, server) {
    // 給這個請求最多 60 秒的無活動時間,而不是預設的 10 秒
    server.timeout(req, 60);

    // 如果請求體傳送超過 60 秒,則請求將被中止
    await req.text();

    return new Response("Done!");
  },
});

使用 server.timeout(req, 0) 可讓長時間執行的流式響應(例如伺服器傳送事件)保持連線,而無需為每個請求調高全域性 idleTimeout。在 HTTP/2 上,超時作用於連線,因此其開啟的請求中最寬鬆的超時值會生效:

TypeScript
Bun.serve({
  routes: {
    "/events": (req, server) => {
      // 為此流式響應停用空閒超時。
      // 否則如果 10 秒內(預設的 idleTimeout)
      // 沒有傳送位元組,連線將被關閉。
      server.timeout(req, 0);

      return new Response(
        async function* () {
          yield "data: hello\n\n";
          // 事件可以零星到達,而不會導致連線被終止
        },
        { headers: { "Content-Type": "text/event-stream" } },
      );
    },
  },
});

server.requestIP(Request)

獲取客戶端 IP 和埠資訊:

TypeScript
const server = Bun.serve({
  fetch(req, server) {
    const address = server.requestIP(req);
    if (address) {
      return new Response(`Client IP: ${address.address}, Port: ${address.port}`);
    }
    return new Response("Unknown client");
  },
});

對於已關閉的請求或 Unix 域套接字,傳回 null。


伺服器指標

server.pendingRequests 和 server.pendingWebSockets

使用內建計數器監控伺服器活動:

TypeScript
const server = Bun.serve({
  fetch(req, server) {
    return new Response(
      `活動請求數:${server.pendingRequests}\n` + `活動 WebSocket 數:${server.pendingWebSockets}`,
    );
  },
});

server.subscriberCount(topic)

獲取某個 WebSocket 主題的訂閱者數量:

TypeScript
const server = Bun.serve({
  fetch(req, server) {
    const chatUsers = server.subscriberCount("chat");
    return new Response(`${chatUsers} 位使用者正在聊天`);
  },
  websocket: {
    message(ws) {
      ws.subscribe("chat");
    },
  },
});

效能基準

以下 Bun 和 Node.js 伺服器會對每個傳入的 Request 響應 Bun!。

Bun
Bun.serve({
  fetch(req: Request) {
    return new Response("Bun!");
  },
  port: 3000,
});
TypeScript
require("http")
  .createServer((req, res) => res.end("Bun!"))
  .listen(8080);

在 Linux 上,Bun.serve 伺服器每秒可處理請求數大約是 Node.js 的 2.5 倍。

執行環境每秒請求數
Node 16~64,000
Bun~160,000

影像


例項示範:REST API

這是使用 Bun 路由的基本資料庫驅動 REST API,無任何相依:

server.ts
import type { Post } from "./types.ts";
import { Database } from "bun:sqlite";

const db = new Database("posts.db");
db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT NOT NULL
  )
`);

Bun.serve({
  routes: {
    // 列出帖子
    "/api/posts": {
      GET: () => {
        const posts = db.query("SELECT * FROM posts").all();
        return Response.json(posts);
      },

      // 建立帖子
      POST: async req => {
        const post: Omit<Post, "id" | "created_at"> = await req.json();
        const id = crypto.randomUUID();

        db.query(
          `INSERT INTO posts (id, title, content, created_at)
           VALUES (?, ?, ?, ?)`,
        ).run(id, post.title, post.content, new Date().toISOString());

        return Response.json({ id, ...post }, { status: 201 });
      },
    },

    // 根據 ID 獲取帖子
    "/api/posts/:id": req => {
      const post = db.query("SELECT * FROM posts WHERE id = ?").get(req.params.id);

      if (!post) {
        return new Response("未找到", { status: 404 });
      }

      return Response.json(post);
    },
  },

  error(error) {
    console.error(error);
    return new Response("內部伺服器錯誤", { status: 500 });
  },
});
types.ts
export interface Post {
  id: string;
  title: string;
  content: string;
  created_at: string;
}

參考

expandable
interface Server extends Disposable {
  /**
   * 停止伺服器接受新連線。
   * @param closeActiveConnections 如果為 true,則立即終止所有連線
   * @returns 伺服器停止時解析的 Promise
   */
  stop(closeActiveConnections?: boolean): Promise<void>;

  /**
   * Close idle keep-alive connections without stopping the server.
   * @returns The number of connections closed
   */
  closeIdleConnections(): number;

  /**
   * Update handlers without restarting the server.
   * Only fetch, error, routes, and websocket can be updated.
   */
  reload(options: Serve.Options<undefined>): Server;

  /**
   * 向執行中的伺服器發出請求。
   * 用於測試或內部路由。
   */
  fetch(request: Request | string): Response | Promise<Response>;

  /**
   * 將 HTTP 請求升級為 WebSocket 連線。
   * @returns 升級成功時為 true,否則為 false
   */
  upgrade<T = undefined>(
    request: Request,
    options?: {
      headers?: Bun.HeadersInit;
      data?: T;
    },
  ): boolean;

  /**
   * 向所有訂閱了某個主題的 WebSocket 客戶端釋出訊息。
   * @returns 傳送的位元組數;如果訊息被丟棄則為 0;如果應用了背壓則為 -1
   */
  publish(
    topic: string,
    data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
    compress?: boolean,
  ): ServerWebSocketSendStatus;

  /**
   * 獲取訂閱了某個主題的 WebSocket 客戶端數量。
   */
  subscriberCount(topic: string): number;

  /**
   * 獲取客戶端 IP 地址和埠。
   * @returns 對於已關閉的請求或 Unix 套接字,傳回 null
   */
  requestIP(request: Request): SocketAddress | null;

  /**
   * 為請求設定自定義空閒超時時間。
   * @param seconds 超時時間(以秒為單位),設為 0 可停用
   */
  timeout(request: Request, seconds: number): void;

  /**
   * 伺服器執行環境保持行程處於活動狀態。
   */
  ref(): void;

  /**
   * 允許伺服器作為唯一執行中的執行環境行程退出。
   */
  unref(): void;

  /** 當前正在處理的 HTTP 請求數量 */
  readonly pendingRequests: number;

  /** 當前活動的 WebSocket 連線數量 */
  readonly pendingWebSockets: number;

  /** 完整的伺服器 URL,包括協議、主機名和埠 */
  readonly url: URL;

  /** 伺服器正在監聽的埠 */
  readonly port: number;

  /** 伺服器繫結到的主機名 */
  readonly hostname: string;

  /** 伺服器是否處於開發模式 */
  readonly development: boolean;

  /** 伺服器例項 ID */
  readonly id: string;
}

interface WebSocketHandler<T = undefined> {
  /** WebSocket 訊息的最大大小(以位元組為單位) */
  maxPayloadLength?: number;

  /** 應用背壓前排隊訊息的位元組數 */
  backpressureLimit?: number;

  /** 達到背壓限制時是否關閉連線 */
  closeOnBackpressureLimit?: boolean;

  /** 背壓解除時呼叫 */
  drain?(ws: ServerWebSocket<T>): void | Promise<void>;

  /** 空閒超時時間(以秒為單位) */
  idleTimeout?: number;

  /** 啟用按訊息進行的 deflate 壓縮 */
  perMessageDeflate?:
    | boolean
    | {
        compress?: WebSocketCompressor | boolean;
        decompress?: WebSocketCompressor | boolean;
      };

  /** 傳送 Ping 幀以保持連線處於活動狀態 */
  sendPings?: boolean;

  /** Whether ws.publish() also sends the message to the socket that published it, if subscribed */
  publishToSelf?: boolean;

  /** 連線開啟時呼叫 */
  open?(ws: ServerWebSocket<T>): void | Promise<void>;

  /** 收到訊息時呼叫 */
  message(ws: ServerWebSocket<T>, message: string | Buffer): void | Promise<void>;

  /** 連線關閉時呼叫 */
  close?(ws: ServerWebSocket<T>, code: number, reason: string): void | Promise<void>;

  /** 收到 Ping 幀時呼叫 */
  ping?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;

  /** 收到 Pong 幀時呼叫 */
  pong?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;
}

interface TLSOptions {
  /** 證書頒發機構鏈 */
  ca?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** 伺服器證書 */
  cert?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** DH 引數檔案的路徑 */
  dhParamsFile?: string;

  /** 私鑰 */
  key?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** 降低 TLS 記憶體使用量 */
  lowMemoryMode?: boolean;

  /** 私鑰密碼短語 */
  passphrase?: string;

  /** OpenSSL 選項標誌 */
  secureOptions?: number;

  /** SNI 伺服器名稱 */
  serverName?: string;
}