伺服器
使用 Bun.serve 在 Bun 中啟動高效能 HTTP 伺服器
基本設定
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 會從此清單提供資源,無需在執行環境進行打包。
import myReactSinglePageApp from "./index.html";
Bun.serve({
routes: {
"/": myReactSinglePageApp,
},
});
HTML 匯入的功能不止是提供 HTML:它們還會執行 Bun 的 打包器、JavaScript 轉譯器和 CSS 解析器,因此你可以使用 React、TypeScript 和 Tailwind CSS 建置前端。
如需瞭解使用 HTML 匯入建置全棧應用的完整指南,請參閱全棧開發伺服器。
設定
更改 port 和 hostname
要設定伺服器監聽的埠和主機名,請在選項物件中設定 port 和 hostname。
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 來隨機選擇一個可用埠。
const server = Bun.serve({
port: 0, // 隨機埠 // [!code ++]
fetch(req) {
return new Response("404!");
},
});
// server.port 是隨機選擇的埠
console.log(server.port);
從伺服器的 port 或 url 屬性中讀取所選埠。
console.log(server.port); // 3000
console.log(server.url); // http://localhost:3000
設定預設埠
有多個標誌和環境變數可以設定預設埠,當未設定 port 選項時,Bun 會使用該埠。
--port命令列引數
bun --port=4002 server.ts
BUN_PORT環境變數
BUN_PORT=4002 bun server.ts
PORT環境變數
PORT=4002 bun server.ts
NODE_PORT環境變數
NODE_PORT=4002 bun server.ts
Unix 域套接字
要監聽 Unix 域套接字,請傳入 unix 選項並指定套接字路徑。
Bun.serve({
unix: "/tmp/my-socket.sock", // 套接字路徑
fetch(req) {
return new Response(`404!`);
},
});
抽象名稱空間套接字
在 Linux 上,Bun 還支援抽象名稱空間套接字:在 unix 路徑前加上一個空位元組。
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。
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:
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 服務。
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 則完全停用超時。
Bun.serve({
// 30 秒(預設值為 10)
idleTimeout: 30,
fetch(req) {
return new Response("Bun!");
},
});
export default 語法
無需將伺服器選項傳入 Bun.serve,你可以直接 export default 這些選項。
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() 無需重啟伺服器即可更新路由:
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()
停止伺服器接受新連線:
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 連線:
const closed = server.closeIdleConnections();
console.log(`closed ${closed} idle connections`);
它會傳回已關閉的連線數量。正在處理請求的連線和已開啟的 WebSocket 不受影響,伺服器仍會接受新連線。此方法與 node:http 的 server.closeIdleConnections() 行為一致,後者不傳回任何內容。
server.ref() 和 server.unref()
控制伺服器是否保持 Bun 行程存活:
// 如果伺服器是唯一執行的東西,不阻止行程退出
server.unref();
// 恢復預設行為 - 保持行程存活
server.ref();
server.reload()
無需重啟即可更新伺服器處理器:
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 可以完全停用該請求的超時時間。
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 上,超時作用於連線,因此其開啟的請求中最寬鬆的超時值會生效:
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 和埠資訊:
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
使用內建計數器監控伺服器活動:
const server = Bun.serve({
fetch(req, server) {
return new Response(
`活動請求數:${server.pendingRequests}\n` + `活動 WebSocket 數:${server.pendingWebSockets}`,
);
},
});
server.subscriberCount(topic)
獲取某個 WebSocket 主題的訂閱者數量:
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.serve({
fetch(req: Request) {
return new Response("Bun!");
},
port: 3000,
});
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,無任何相依:
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 });
},
});
export interface Post {
id: string;
title: string;
content: string;
created_at: string;
}
參考
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;
}