Bun Fetch API
Bun 内置了符合 Web 标准的 Fetch API,用于发送 HTTP 请求。本章介绍 Bun 的网络请求功能。
基本请求
GET 请求
typescript
// 最简单的 GET 请求
const response = await fetch("https://api.example.com/users");
const data = await response.json();
console.log(data);
// 获取文本
const text = await fetch("https://example.com").then(r => r.text());
// 获取二进制数据
const buffer = await fetch("https://example.com/image.png")
.then(r => r.arrayBuffer());POST 请求
typescript
// 发送 JSON
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "张三",
email: "zhangsan@example.com",
}),
});
const result = await response.json();
console.log(result);其他 HTTP 方法
typescript
// PUT 请求
await fetch("https://api.example.com/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "新名字" }),
});
// PATCH 请求
await fetch("https://api.example.com/users/1", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "new@example.com" }),
});
// DELETE 请求
await fetch("https://api.example.com/users/1", {
method: "DELETE",
});请求配置
完整配置选项
typescript
const response = await fetch("https://api.example.com/data", {
// HTTP 方法
method: "POST",
// 请求头
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer your-token",
"Accept": "application/json",
"User-Agent": "Bun/1.0",
},
// 请求体
body: JSON.stringify({ data: "value" }),
// 重定向处理
redirect: "follow", // "follow" | "error" | "manual"
// 凭证
credentials: "include", // "omit" | "same-origin" | "include"
// 缓存模式
cache: "no-cache",
// 超时(毫秒)- Bun 特有
timeout: 30000,
// TLS 配置 - Bun 特有
tls: {
rejectUnauthorized: true,
},
});请求头
typescript
// 使用 Headers 对象
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer token");
headers.append("Accept-Language", "zh-CN");
const response = await fetch("https://api.example.com", { headers });
// 检查响应头
console.log(response.headers.get("content-type"));
console.log(response.headers.get("x-request-id"));
// 遍历响应头
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}响应处理
Response 对象
typescript
const response = await fetch("https://api.example.com/data");
// 响应状态
console.log("状态码:", response.status); // 200
console.log("状态文本:", response.statusText); // "OK"
console.log("是否成功:", response.ok); // true (200-299)
// 响应 URL
console.log("最终 URL:", response.url);
// 响应类型
console.log("类型:", response.type); // "basic" | "cors" | etc.
// 是否重定向
console.log("已重定向:", response.redirected);读取响应体
typescript
const response = await fetch("https://api.example.com/data");
// JSON
const json = await response.json();
// 文本
const text = await response.text();
// ArrayBuffer
const buffer = await response.arrayBuffer();
// Blob
const blob = await response.blob();
// FormData
const formData = await response.formData();
// 注意:响应体只能读取一次
// 如果需要多次使用,先克隆
const clone = response.clone();
const text1 = await response.text();
const text2 = await clone.text();流式读取
typescript
const response = await fetch("https://example.com/large-file");
// 获取可读流
const reader = response.body?.getReader();
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log("读取块:", value.length, "字节");
}
}表单和文件上传
FormData
typescript
const formData = new FormData();
formData.append("name", "张三");
formData.append("age", "25");
const response = await fetch("https://api.example.com/submit", {
method: "POST",
body: formData,
// 不需要手动设置 Content-Type,fetch 会自动处理
});文件上传
typescript
// 上传本地文件
const file = Bun.file("./document.pdf");
const formData = new FormData();
formData.append("file", file);
formData.append("description", "重要文档");
const response = await fetch("https://api.example.com/upload", {
method: "POST",
body: formData,
});
console.log("上传结果:", await response.json());多文件上传
typescript
const formData = new FormData();
// 添加多个文件
formData.append("files", Bun.file("./image1.png"));
formData.append("files", Bun.file("./image2.png"));
formData.append("files", Bun.file("./image3.png"));
const response = await fetch("https://api.example.com/upload-multiple", {
method: "POST",
body: formData,
});错误处理
基本错误处理
typescript
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof TypeError) {
console.error("网络错误:", error.message);
} else {
console.error("请求失败:", error);
}
}超时处理
typescript
// 方法 1: 使用 Bun 的 timeout 选项
const response = await fetch("https://api.example.com/slow", {
timeout: 5000, // 5 秒超时
});
// 方法 2: 使用 AbortController
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch("https://api.example.com/slow", {
signal: controller.signal,
});
clearTimeout(timeout);
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
console.error("请求超时");
}
throw error;
}取消请求
typescript
const controller = new AbortController();
// 开始请求
const fetchPromise = fetch("https://api.example.com/data", {
signal: controller.signal,
});
// 在某个条件下取消
setTimeout(() => {
controller.abort();
console.log("请求已取消");
}, 1000);
try {
const response = await fetchPromise;
} catch (error) {
if (error.name === "AbortError") {
console.log("请求被取消");
}
}重试机制
简单重试
typescript
async function fetchWithRetry(
url: string,
options: RequestInit = {},
maxRetries = 3
): Promise<Response> {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// 服务器错误,可以重试
if (response.status >= 500) {
lastError = new Error(`Server error: ${response.status}`);
continue;
}
// 客户端错误,不重试
return response;
} catch (error) {
lastError = error as Error;
console.log(`尝试 ${i + 1} 失败:`, error);
// 等待后重试
await Bun.sleep(1000 * (i + 1));
}
}
throw lastError || new Error("请求失败");
}
// 使用
const response = await fetchWithRetry("https://api.example.com/data");指数退避
typescript
async function fetchWithExponentialBackoff(
url: string,
options: RequestInit = {},
maxRetries = 5
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
if (response.status < 500) return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
}
// 指数退避: 1s, 2s, 4s, 8s, 16s
const delay = Math.min(1000 * Math.pow(2, i), 30000);
const jitter = Math.random() * 1000;
await Bun.sleep(delay + jitter);
}
throw new Error("Max retries exceeded");
}HTTP 客户端封装
API 客户端
typescript
class ApiClient {
private baseUrl: string;
private headers: Record<string, string>;
constructor(baseUrl: string, headers: Record<string, string> = {}) {
this.baseUrl = baseUrl;
this.headers = {
"Content-Type": "application/json",
...headers,
};
}
setHeader(key: string, value: string) {
this.headers[key] = value;
}
setToken(token: string) {
this.headers["Authorization"] = `Bearer ${token}`;
}
private async request<T>(
method: string,
path: string,
body?: unknown
): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.message || response.statusText);
}
return response.json();
}
get<T>(path: string): Promise<T> {
return this.request<T>("GET", path);
}
post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>("POST", path, body);
}
put<T>(path: string, body: unknown): Promise<T> {
return this.request<T>("PUT", path, body);
}
patch<T>(path: string, body: unknown): Promise<T> {
return this.request<T>("PATCH", path, body);
}
delete<T>(path: string): Promise<T> {
return this.request<T>("DELETE", path);
}
}
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = "ApiError";
}
}
// 使用
const api = new ApiClient("https://api.example.com");
api.setToken("your-auth-token");
const users = await api.get<User[]>("/users");
const newUser = await api.post<User>("/users", { name: "张三" });并发请求
Promise.all
typescript
// 并行请求多个 API
const [users, posts, comments] = await Promise.all([
fetch("https://api.example.com/users").then(r => r.json()),
fetch("https://api.example.com/posts").then(r => r.json()),
fetch("https://api.example.com/comments").then(r => r.json()),
]);
console.log("用户:", users.length);
console.log("文章:", posts.length);
console.log("评论:", comments.length);Promise.allSettled
typescript
// 即使部分失败也继续
const results = await Promise.allSettled([
fetch("https://api.example.com/users").then(r => r.json()),
fetch("https://api.example.com/may-fail").then(r => r.json()),
fetch("https://api.example.com/posts").then(r => r.json()),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`请求 ${index} 成功:`, result.value);
} else {
console.log(`请求 ${index} 失败:`, result.reason);
}
});限制并发数
typescript
async function fetchWithConcurrency<T>(
urls: string[],
concurrency: number,
fetcher: (url: string) => Promise<T>
): Promise<T[]> {
const results: T[] = [];
const executing: Promise<void>[] = [];
for (const url of urls) {
const promise = fetcher(url).then(result => {
results.push(result);
});
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
executing.splice(
executing.findIndex(p => p === promise),
1
);
}
}
await Promise.all(executing);
return results;
}
// 使用:最多同时 3 个请求
const urls = [
"https://api.example.com/1",
"https://api.example.com/2",
"https://api.example.com/3",
"https://api.example.com/4",
"https://api.example.com/5",
];
const results = await fetchWithConcurrency(
urls,
3,
url => fetch(url).then(r => r.json())
);代理和 TLS
HTTP 代理
typescript
// 使用环境变量
process.env.HTTP_PROXY = "http://proxy.example.com:8080";
process.env.HTTPS_PROXY = "http://proxy.example.com:8080";
const response = await fetch("https://api.example.com");自定义 TLS
typescript
const response = await fetch("https://internal-api.company.com", {
tls: {
// 自签名证书
rejectUnauthorized: false,
// 或指定 CA
ca: Bun.file("./certs/ca.pem"),
},
});下载文件
typescript
// 下载并保存文件
async function downloadFile(url: string, savePath: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`下载失败: ${response.status}`);
}
await Bun.write(savePath, response);
console.log(`文件已保存到: ${savePath}`);
}
// 使用
await downloadFile(
"https://example.com/large-file.zip",
"./downloads/file.zip"
);下载进度
typescript
async function downloadWithProgress(url: string, savePath: string) {
const response = await fetch(url);
const contentLength = response.headers.get("content-length");
const total = contentLength ? parseInt(contentLength) : 0;
let downloaded = 0;
const reader = response.body?.getReader();
const chunks: Uint8Array[] = [];
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
downloaded += value.length;
if (total) {
const percent = ((downloaded / total) * 100).toFixed(1);
console.log(`下载进度: ${percent}%`);
}
}
}
// 合并并保存
const blob = new Blob(chunks);
await Bun.write(savePath, blob);
}小结
本章介绍了:
- ✅ 基本 HTTP 请求方法
- ✅ 请求配置和响应处理
- ✅ 表单和文件上传
- ✅ 错误处理和超时
- ✅ 重试机制和并发控制
- ✅ HTTP 客户端封装
- ✅ 文件下载
下一步
继续阅读 打包构建 了解 Bun 的内置打包工具。