要开始使用,导入 HTML 文件并将它们传递给 Bun.serve() 的 routes 选项。
import { serve } from "bun";
import dashboard from "./dashboard.html";
import homepage from "./index.html";
const server = serve({
routes: {
// ** HTML 导入 **
// 将 index.html 打包并路由到 "/"。这使用 HTMLRewriter 扫描
// HTML 中的 `<script>` 和 `<link>` 标签,对它们运行 Bun 的 JavaScript
// 和 CSS 打包器,转译任何 TypeScript、JSX 和 TSX,
// 使用 Bun 的 CSS 解析器降级 CSS 并提供结果。
"/": homepage,
// 将 dashboard.html 打包并路由到 "/dashboard"
"/dashboard": dashboard,
// ** API 端点 **(需要 Bun v1.2.3+)
"/api/users": {
async GET(req) {
const users = await sql`SELECT * FROM users`;
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
const [user] = await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;
return Response.json(user);
},
},
"/api/users/:id": async req => {
const { id } = req.params;
const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;
return Response.json(user);
},
},
// 启用开发模式以获得:
// - 详细的错误消息
// - 热重载(需要 Bun v1.2.3+)
development: true,
});
console.log(`Listening on ${server.url}`);bun run app.tsHTML 路由
HTML 导入作为路由
Web 从 HTML 开始,Bun 的全栈开发服务器也是如此。
要指定前端的入口点,将 HTML 文件导入到您的 JavaScript/TypeScript/TSX/JSX 文件中。
import dashboard from "./dashboard.html";
import homepage from "./index.html";这些 HTML 文件用作 Bun 开发服务器中的路由,您可以传递给 Bun.serve()。
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
fetch(req) {
// ... api 请求
},
});当您向 /dashboard 或 / 发出请求时,Bun 会自动打包 HTML 文件中的 <script> 和 <link> 标签,将它们暴露为静态路由,并提供结果。
HTML 处理示例
像这样的 index.html 文件:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="./reset.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./sentry-and-preloads.ts"></script>
<script type="module" src="./my-app.tsx"></script>
</body>
</html>变成像这样:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="/index-[hash].css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/index-[hash].js"></script>
</body>
</html>React 集成
要在客户端代码中使用 React,导入 react-dom/client 并渲染您的应用。
import dashboard from "../public/dashboard.html";
import { serve } from "bun";
serve({
routes: {
"/": dashboard,
},
async fetch(req) {
// ...api 请求
return new Response("hello world");
},
});import { createRoot } from 'react-dom/client';
import App from './app';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/frontend.tsx"></script>
</body>
</html>import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
);
}开发模式
在本地构建时,通过在 Bun.serve() 中设置 development: true 启用开发模式。
import homepage from "./index.html";
import dashboard from "./dashboard.html";
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
development: true,
fetch(req) {
// ... api 请求
},
});开发模式功能
当 development 为 true 时,Bun 将:
- 在响应中包含 SourceMap 头,以便开发工具可以显示原始源代码
- 禁用压缩
- 在每次请求
.html文件时重新打包资源 - 启用热模块重载(除非设置了
hmr: false) - 将浏览器控制台日志回显到终端
高级开发配置
Bun.serve() 支持将浏览器控制台日志回显到终端。
要启用此功能,在 Bun.serve() 的 development 对象中传递 console: true。
import homepage from "./index.html";
Bun.serve({
// development 也可以是对象。
development: {
// 启用热模块重载
hmr: true,
// 将浏览器控制台日志回显到终端
console: true,
},
routes: {
"/": homepage,
},
});当设置 console: true 时,Bun 会将控制台日志从浏览器流式传输到终端。这重用 HMR 的现有 WebSocket 连接来发送日志。
开发与生产
| 功能 | 开发 | 生产 |
|---|---|---|
| Source maps | ✅ 启用 | ❌ 禁用 |
| 压缩 | ❌ 禁用 | ✅ 启用 |
| 热重载 | ✅ 启用 | ❌ 禁用 |
| 资源打包 | 🔄 每次请求 | 💾 缓存 |
| 控制台日志 | 🖥️ 浏览器 → 终端 | ❌ 禁用 |
| 错误详情 | 📝 详细 | 🔒 最小化 |
生产模式
热重载和 development: true 帮助您快速迭代,但在生产中,您的服务器应该尽可能快并尽可能少的外部依赖。
提前打包(推荐)
从 Bun v1.2.17 开始,您可以使用 Bun.build 或 bun build 提前打包您的全栈应用程序。
bun build --target=bun --production --outdir=dist ./src/index.ts当 Bun 的打包器看到来自服务器端代码的 HTML 导入时,它会将引用的 JavaScript/TypeScript/TSX/JSX 和 CSS 文件打包为清单对象,Bun.serve() 可以使用该对象来提供资源。
import { serve } from "bun";
import index from "./index.html";
serve({
routes: { "/": index },
});运行时打包
当添加构建步骤太复杂时,您可以在 Bun.serve() 中设置 development: false。
这将:
- 启用打包资源的内存缓存。Bun 将在第一次请求
.html文件时延迟打包资源,并将结果缓存在内存中直到服务器重启。 - 启用
Cache-Control头和ETag头 - 压缩 JavaScript/TypeScript/TSX/JSX 文件
import { serve } from "bun";
import homepage from "./index.html";
serve({
routes: {
"/": homepage,
},
// 生产模式
development: false,
});API 路由
HTTP 方法处理程序
使用 HTTP 方法处理程序定义 API 端点:
import { serve } from "bun";
serve({
routes: {
"/api/users": {
async GET(req) {
// 处理 GET 请求
const users = await getUsers();
return Response.json(users);
},
async POST(req) {
// 处理 POST 请求
const userData = await req.json();
const user = await createUser(userData);
return Response.json(user, { status: 201 });
},
async PUT(req) {
// 处理 PUT 请求
const userData = await req.json();
const user = await updateUser(userData);
return Response.json(user);
},
async DELETE(req) {
// 处理 DELETE 请求
await deleteUser(req.params.id);
return new Response(null, { status: 204 });
},
},
},
});动态路由
在路由中使用 URL 参数:
serve({
routes: {
// 单个参数
"/api/users/:id": async req => {
const { id } = req.params;
const user = await getUserById(id);
return Response.json(user);
},
// 多个参数
"/api/users/:userId/posts/:postId": async req => {
const { userId, postId } = req.params;
const post = await getPostByUser(userId, postId);
return Response.json(post);
},
// 通配符路由
"/api/files/*": async req => {
const filePath = req.params["*"];
const file = await getFile(filePath);
return new Response(file);
},
},
});请求处理
serve({
routes: {
"/api/data": {
async POST(req) {
// 解析 JSON 主体
const body = await req.json();
// 访问头
const auth = req.headers.get("Authorization");
// 访问 URL 参数
const { id } = req.params;
// 访问查询参数
const url = new URL(req.url);
const page = url.searchParams.get("page") || "1";
// 返回响应
return Response.json({
message: "Data processed",
page: parseInt(page),
authenticated: !!auth,
});
},
},
},
});插件
打包静态路由时也支持 Bun 的打包器插件。
要为 Bun.serve 配置插件,在 bunfig.toml 的 [serve.static] 部分中添加 plugins 数组。
TailwindCSS 插件
您可以通过安装和添加 tailwindcss 包和 bun-plugin-tailwind 插件来使用 TailwindCSS。
bun add tailwindcss bun-plugin-tailwind[serve.static]
plugins = ["bun-plugin-tailwind"]这将允许您在 HTML 和 CSS 文件中使用 TailwindCSS 实用程序类。您需要做的就是导入 tailwindcss:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="tailwindcss" />
</head>
<!-- 其余的 HTML... -->
</html>或者,您可以在 CSS 文件中导入 TailwindCSS:
@import "tailwindcss";
.custom-class {
@apply bg-red-500 text-white;
}<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./style.css" />
</head>
<!-- 其余的 HTML... -->
</html>自定义插件
任何导出有效打包器插件对象(基本上是带有 name 和 setup 字段的对象)的 JS 文件或模块都可以放在 plugins 数组中:
[serve.static]
plugins = ["./my-plugin-implementation.ts"]import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "my-custom-plugin",
setup(build) {
// 插件实现
build.onLoad({ filter: /\.custom$/ }, async args => {
const text = await Bun.file(args.path).text();
return {
contents: `export default ${JSON.stringify(text)};`,
loader: "js",
};
});
},
};
export default myPlugin;Bun 将延迟解析和加载每个插件,并使用它们来打包您的路由。
NOTE
这目前在 `bunfig.toml` 中,以便在我们最终将其与 `bun build` CLI 集成时可以静态知道使用了哪些插件。这些插件在 `Bun.build()` 的 JS API 中有效,但在 CLI 中尚不支持。内联环境变量
Bun 可以在构建时用实际值替换前端 JavaScript 和 TypeScript 中的 process.env.* 引用。在 bunfig.toml 中配置 env 选项:
[serve.static]
env = "PUBLIC_*" # 仅内联以 PUBLIC_ 开头的环境变量(推荐)
# env = "inline" # 内联所有环境变量
# env = "disable" # 禁用环境变量替换(默认)注意
这仅适用于字面量 process.env.FOO 引用,不适用于 import.meta.env 或间接访问如 const env = process.env; env.FOO。
如果环境变量未设置,您可能会在浏览器中看到运行时错误,如 ReferenceError: process is not defined。
有关构建时配置和示例的更多详细信息,请参阅 HTML 和静态站点文档。
工作原理
Bun 使用 HTMLRewriter 扫描 HTML 文件中的 <script> 和 <link> 标签,将它们用作 Bun 打包器的入口点,为 JavaScript/TypeScript/TSX/JSX 和 CSS 文件生成优化的打包,并提供结果。
处理流程
1. script 处理
- 转译
<script>标签中的 TypeScript、JSX 和 TSX - 打包导入的依赖项
- 生成 sourcemaps 用于调试
- 当
Bun.serve()中development不为true时压缩
<script type="module" src="./counter.tsx"></script>2. 处理
- 处理 CSS 导入和
<link>标签 - 连接 CSS 文件
- 重写 url 和资源路径以在 URL 中包含内容寻址哈希
<link rel="stylesheet" href="./styles.css" />3.
和资源处理
- 重写资源链接以在 URL 中包含内容寻址哈希
- CSS 文件中的小资源内联到
data:URL 中,减少通过网络发送的 HTTP 请求总数
4. HTML 重写
- 将所有
<script>标签合并为单个<script>标签,URL 中包含内容寻址哈希 - 将所有
<link>标签合并为单个<link>标签,URL 中包含内容寻址哈希 - 输出新的 HTML 文件
5. 服务
- 打包器的所有输出文件都作为静态路由暴露,使用与在
Bun.serve()中将 Response 对象传递给static时相同的内部机制。 - 这与
Bun.build处理 HTML 文件的方式类似。
完整示例
这是一个完整的全栈应用程序示例:
import { serve } from "bun";
import { Database } from "bun:sqlite";
import homepage from "./public/index.html";
import dashboard from "./public/dashboard.html";
// 初始化数据库
const db = new Database("app.db");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const server = serve({
routes: {
// 前端路由
"/": homepage,
"/dashboard": dashboard,
// API 路由
"/api/users": {
async GET() {
const users = db.query("SELECT * FROM users").all();
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
try {
const result = db.query("INSERT INTO users (name, email) VALUES (?, ?) RETURNING *").get(name, email);
return Response.json(result, { status: 201 });
} catch (error) {
return Response.json({ error: "Email already exists" }, { status: 400 });
}
},
},
"/api/users/:id": {
async GET(req) {
const { id } = req.params;
const user = db.query("SELECT * FROM users WHERE id = ?").get(id);
if (!user) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return Response.json(user);
},
async DELETE(req) {
const { id } = req.params;
const result = db.query("DELETE FROM users WHERE id = ?").run(id);
if (result.changes === 0) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return new Response(null, { status: 204 });
},
},
// 健康检查端点
"/api/health": {
GET() {
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
});
},
},
},
// 启用开发模式
development: {
hmr: true,
console: true,
},
// 未匹配路由的回退
fetch(req) {
return new Response("Not Found", { status: 404 });
},
});
console.log(`🚀 Server running on ${server.url}`);<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fullstack Bun App</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/main.tsx"></script>
</body>
</html>import { createRoot } from "react-dom/client";
import { App } from "./App";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(<App />);import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export function App() {
const [users, setUsers] = useState<User[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const fetchUsers = async () => {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
};
const createUser = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email }),
});
if (response.ok) {
setName("");
setEmail("");
await fetchUsers();
} else {
const error = await response.json();
alert(error.error);
}
} catch (error) {
alert("Failed to create user");
} finally {
setLoading(false);
}
};
const deleteUser = async (id: number) => {
if (!confirm("Are you sure?")) return;
try {
const response = await fetch(`/api/users/${id}`, {
method: "DELETE",
});
if (response.ok) {
await fetchUsers();
}
} catch (error) {
alert("Failed to delete user");
}
};
useEffect(() => {
fetchUsers();
}, []);
return (
<div className="container">
<h1>User Management</h1>
<form onSubmit={createUser} className="form">
<input type="text" placeholder="Name" value={name} onChange={e => setName(e.target.value)} required />
<input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required />
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create User"}
</button>
</form>
<div className="users">
<h2>Users ({users.length})</h2>
{users.map(user => (
<div key={user.id} className="user-card">
<div>
<strong>{user.name}</strong>
<br />
<span>{user.email}</span>
</div>
<button onClick={() => deleteUser(user.id)} className="delete-btn">
Delete
</button>
</div>
))}
</div>
</div>
);
}* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #2563eb;
margin-bottom: 2rem;
}
.form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.form input {
flex: 1;
min-width: 200px;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form button {
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.form button:hover {
background: #1d4ed8;
}
.form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.users {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.user-card:last-child {
border-bottom: none;
}
.delete-btn {
padding: 0.5rem 1rem;
background: #dc2626;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #b91c1c;
}最佳实践
项目结构
my-app/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ └── UserList.tsx
│ ├── styles/
│ │ ├── globals.css
│ │ └── components.css
│ ├── utils/
│ │ └── api.ts
│ ├── App.tsx
│ └── main.tsx
├── public/
│ ├── index.html
│ ├── dashboard.html
│ └── favicon.ico
├── server/
│ ├── routes/
│ │ ├── users.ts
│ │ └── auth.ts
│ ├── db/
│ │ └── schema.sql
│ └── index.ts
├── bunfig.toml
└── package.json基于环境的配置
export const config = {
development: process.env.NODE_ENV !== "production",
port: process.env.PORT || 3000,
database: {
url: process.env.DATABASE_URL || "./dev.db",
},
cors: {
origin: process.env.CORS_ORIGIN || "*",
},
};错误处理
export function errorHandler(error: Error, req: Request) {
console.error("Server error:", error);
if (process.env.NODE_ENV === "production") {
return Response.json({ error: "Internal server error" }, { status: 500 });
}
return Response.json(
{
error: error.message,
stack: error.stack,
},
{ status: 500 },
);
}API 响应助手
export function json(data: any, status = 200) {
return Response.json(data, { status });
}
export function error(message: string, status = 400) {
return Response.json({ error: message }, { status });
}
export function notFound(message = "Not found") {
return error(message, 404);
}
export function unauthorized(message = "Unauthorized") {
return error(message, 401);
}类型安全
export interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export interface CreateUserRequest {
name: string;
email: string;
}
export interface ApiResponse<T> {
data?: T;
error?: string;
}部署
生产构建
# 为生产构建
bun build --target=bun --production --outdir=dist ./server/index.ts
# 运行生产服务器
NODE_ENV=production bun dist/index.jsDocker 部署
FROM oven/bun:1 as base
WORKDIR /usr/src/app
# 安装依赖项
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# 复制源代码
COPY . .
# 构建应用程序
RUN bun build --target=bun --production --outdir=dist ./server/index.ts
# 生产阶段
FROM oven/bun:1-slim
WORKDIR /usr/src/app
COPY --from=base /usr/src/app/dist ./
COPY --from=base /usr/src/app/public ./public
EXPOSE 3000
CMD ["bun", "index.js"]环境变量
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
CORS_ORIGIN=https://myapp.com从其他框架迁移
从 Express + Webpack 迁移
// 之前(Express + Webpack)
app.use(express.static("dist"));
app.get("/api/users", (req, res) => {
res.json(users);
});
// 之后(Bun 全栈)
serve({
routes: {
"/": homepage, // 替换 express.static
"/api/users": {
GET() {
return Response.json(users);
},
},
},
});从 Next.js API 路由迁移
// 之前(Next.js)
export default function handler(req, res) {
if (req.method === 'GET') {
res.json(users);
}
}
// 之后(Bun)
"/api/users": {
GET() { return Response.json(users); }
}限制和未来计划
当前限制
bun buildCLI 集成尚不适用于全栈应用程序- API 路由的自动发现未实现
- 服务器端渲染(SSR)未内置
计划功能
- 与
bun buildCLI 集成 - 基于文件系统的 API 端点路由
- 内置 SSR 支持
- 增强的插件生态系统
NOTE
这是一项正在进行的工作。随着 Bun 的不断发展,功能和 API 可能会发生变化。