一、Fetch API 简介
Fetch API 是现代浏览器提供的一个用于网络请求的 JavaScript API,它提供了一个更强大、更灵活的替代方案来替代传统的 XMLHttpRequest。Fetch API 基于 Promise,支持现代的异步编程模式,使网络请求代码更加简洁和易于维护。
二、基本用法
2.1 发送 GET 请求
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
2.2 发送 POST 请求
fetch("https://api.example.com/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key: "value" }),
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
三、参数说明
Fetch API 的 fetch() 函数接受两个参数:
- url:必需,请求的 URL 字符串
- options:可选,配置对象,包含以下属性:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| method | HTTP 请求方法 | String | ‘GET’ |
| headers | 请求头信息 | Object/Headers | {} |
| body | 请求体数据 | String/FormData/Blob/BufferSource/URLSearchParams | null |
| mode | 请求模式 | ‘cors’/‘no-cors’/‘same-origin’ | ‘cors’ |
| credentials | 凭证处理方式 | ‘omit’/‘same-origin’/‘include’ | ‘same-origin’ |
| cache | 缓存模式 | ‘default’/‘no-store’/‘reload’/‘no-cache’/‘force-cache’/‘only-if-cached’ | ‘default’ |
| redirect | 重定向处理 | ‘follow’/‘error’/‘manual’ | ‘follow’ |
| referrer | 来源信息 | String | ‘about:client’ |
| referrerPolicy | 来源策略 | String | ‘’ |
| integrity | 子资源完整性 | String | ‘’ |
| keepalive | 是否保持连接 | Boolean | false |
| signal | 中止信号 | AbortSignal | null |
四、响应处理
Fetch API 的响应对象包含以下方法来处理不同类型的响应数据:
4.1 处理 JSON 数据
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data));
4.2 处理文本数据
fetch("https://api.example.com/text")
.then(response => response.text())
.then(text => console.log(text));
4.3 处理 Blob 数据(如图片、视频等)
fetch("https://api.example.com/image.jpg")
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const img = document.createElement("img");
img.src = url;
document.body.appendChild(img);
});
4.4 处理 FormData
fetch("https://api.example.com/form")
.then(response => response.formData())
.then(formData => {
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
});
五、错误处理
Fetch API 只会在网络错误时 reject promise,对于 HTTP 错误(如 404、500 等)不会自动 reject。需要手动检查响应状态:
fetch("https://api.example.com/data")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
六、高级错误处理
6.1 错误分类与处理
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
// 处理 HTTP 错误
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const error = new Error(`HTTP error! status: ${response.status}`);
error.status = response.status;
error.data = errorData;
throw error;
}
// 根据 Content-Type 自动处理响应
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
} else if (contentType && contentType.includes("text/")) {
return await response.text();
} else {
return await response.blob();
}
} catch (error) {
// 分类处理错误
if (error.name === "AbortError") {
console.log("Request aborted");
throw new Error("Request was aborted");
} else if (error.message.includes("NetworkError")) {
console.log("Network error");
throw new Error("Network error, please check your connection");
} else if (error.status) {
// HTTP 错误
console.log(`HTTP error: ${error.status}`);
throw error;
} else {
// 其他错误
console.log("Unknown error:", error);
throw new Error("An unknown error occurred");
}
}
}
6.2 错误重试机制
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fetchWithErrorHandling(url, options);
} catch (error) {
// 只对网络错误重试
if (error.message.includes("Network error") && i < retries - 1) {
console.log(`Retry ${i + 1}/${retries}...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // 指数退避
} else {
throw error;
}
}
}
}
// 使用示例
fetchWithRetry("https://api.example.com/data", {}, 3)
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
6.3 错误处理中间件
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
// 统一错误处理
this.handleError(error);
throw error;
}
}
handleError(error) {
// 可以在这里添加错误日志、错误上报等
console.error("API Error:", error);
// 处理特定错误
if (error.message.includes("401")) {
// 处理未授权错误,如跳转到登录页
console.log("Unauthorized, redirecting to login...");
} else if (error.message.includes("403")) {
// 处理权限错误
console.log("Forbidden, insufficient permissions");
}
}
// 便捷方法
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: "GET" });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: "POST",
body: JSON.stringify(data),
});
}
}
// 使用示例
const api = new ApiClient("https://api.example.com");
api.get("/data")
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
七、高级用法
7.1 自定义请求头
fetch("https://api.example.com/data", {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token123",
"X-Custom-Header": "value",
},
})
.then(response => response.json())
.then(data => console.log(data));
7.2 中止请求
使用 AbortController 可以中止正在进行的 Fetch 请求:
const controller = new AbortController();
const signal = controller.signal;
fetch("https://api.example.com/data", {
signal,
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === "AbortError") {
console.log("Request aborted");
} else {
console.error("Error:", error);
}
});
// 中止请求
setTimeout(() => controller.abort(), 1000);
八、更详细的请求配置
8.1 缓存模式详解
| 缓存模式 | 描述 | 使用场景 |
|---|---|---|
default |
浏览器默认行为,先检查缓存,无缓存则请求 | 常规请求 |
no-store |
完全不使用缓存,每次都请求新数据 | 实时数据、敏感信息 |
reload |
从服务器获取新数据,同时更新缓存 | 强制刷新数据 |
no-cache |
先检查缓存,然后向服务器验证,有变化则更新 | 需要最新数据但允许缓存 |
force-cache |
优先使用缓存,无缓存才请求 | 对实时性要求不高的数据 |
only-if-cached |
只使用缓存,无缓存则失败 | 离线应用 |
8.2 凭证处理
// 不发送凭证(如 cookies)
fetch("https://api.example.com/data", {
credentials: "omit",
});
// 仅在同域请求中发送凭证
fetch("https://api.example.com/data", {
credentials: "same-origin",
});
// 始终发送凭证(包括跨域请求)
fetch("https://api.example.com/data", {
credentials: "include",
});
8.3 请求模式
// 允许跨域请求
fetch("https://api.example.com/data", {
mode: "cors",
});
// 限制为同域请求
fetch("https://api.example.com/data", {
mode: "same-origin",
});
// 无 CORS 头的跨域请求(响应不可读)
fetch("https://api.example.com/data", {
mode: "no-cors",
});
8.4 重定向处理
// 自动跟随重定向
fetch("https://api.example.com/data", {
redirect: "follow",
});
// 遇到重定向时抛出错误
fetch("https://api.example.com/data", {
redirect: "error",
});
// 手动处理重定向
fetch("https://api.example.com/data", {
redirect: "manual",
});
8.5 高级缓存策略(IndexedDB)
// 使用 IndexedDB 缓存响应
class ApiCache {
constructor(dbName = "api-cache", version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = event => {
const db = event.target.result;
if (!db.objectStoreNames.contains("cache")) {
db.createObjectStore("cache", { keyPath: "url" });
}
};
});
}
async get(url) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction("cache", "readonly");
const store = transaction.objectStore("cache");
const request = store.get(url);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
if (request.result) {
// 检查缓存是否过期(1小时)
const now = Date.now();
const cacheTime = request.result.timestamp;
if (now - cacheTime < 60 * 60 * 1000) {
resolve(request.result.data);
} else {
// 缓存过期
resolve(null);
}
} else {
resolve(null);
}
};
});
}
async set(url, data) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction("cache", "readwrite");
const store = transaction.objectStore("cache");
const request = store.put({
url,
data,
timestamp: Date.now(),
});
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async fetchWithCache(url, options = {}) {
// 尝试从缓存获取
const cachedData = await this.get(url);
if (cachedData) {
console.log("Using cached data for", url);
return cachedData;
}
// 从网络获取
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 缓存响应
await this.set(url, data);
return data;
}
}
// 使用示例
const cache = new ApiCache();
cache
.fetchWithCache("https://api.example.com/data")
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
九、进度监控
9.1 下载进度监控
async function downloadWithProgress(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentLength = response.headers.get("content-length");
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
loaded += value.length;
if (total) {
const progress = Math.round((loaded / total) * 100);
console.log(`Download progress: ${progress}%`);
// 可以更新 UI 进度条
}
}
const blob = new Blob(chunks);
return blob;
}
// 使用示例
downloadWithProgress("https://api.example.com/large-file.zip")
.then(blob => {
console.log("Download complete:", blob.size, "bytes");
})
.catch(error => {
console.error("Error:", error);
});
9.2 上传进度监控
function uploadWithProgress(file, url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file);
xhr.upload.addEventListener("progress", event => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
console.log(`Upload progress: ${progress}%`);
// 可以更新 UI 进度条
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP error! status: ${xhr.status}`));
}
});
xhr.addEventListener("error", () => {
reject(new Error("Network error"));
});
xhr.open("POST", url);
xhr.send(formData);
});
}
// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener("change", async e => {
const file = e.target.files[0];
if (file) {
try {
const result = await uploadWithProgress(file, "https://api.example.com/upload");
console.log("Upload complete:", result);
} catch (error) {
console.error("Error:", error);
}
}
});
十、并发请求处理
10.1 Promise.all 处理多个并行请求
async function fetchMultipleEndpoints() {
try {
const [users, posts, comments] = await Promise.all([
fetch("https://api.example.com/users").then(res => res.json()),
fetch("https://api.example.com/posts").then(res => res.json()),
fetch("https://api.example.com/comments").then(res => res.json()),
]);
console.log("Users:", users);
console.log("Posts:", posts);
console.log("Comments:", comments);
return { users, posts, comments };
} catch (error) {
console.error("Error fetching data:", error);
throw error;
}
}
// 使用示例
fetchMultipleEndpoints()
.then(data => console.log("All data:", data))
.catch(error => console.error("Error:", error));
10.2 Promise.race 处理最快响应的请求
async function fetchWithFallback(primaryUrl, fallbackUrl) {
const controller = new AbortController();
const primaryPromise = fetch(primaryUrl, { signal: controller.signal }).then(res => {
if (!res.ok) throw new Error(`Primary failed: ${res.status}`);
return res.json();
});
const fallbackPromise = fetch(fallbackUrl).then(res => {
if (!res.ok) throw new Error(`Fallback failed: ${res.status}`);
return res.json();
});
let result;
try {
result = await Promise.race([primaryPromise, fallbackPromise]);
} catch (error) {
// 确保中止失败的请求
controller.abort();
throw error;
}
// 竞速获胜后,中止另一个请求
controller.abort();
return result;
}
// 使用示例
fetchWithFallback("https://api.example.com/data", "https://backup-api.example.com/data")
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
十一、与 Axios 的比较
| 特性 | Fetch API | Axios |
|---|---|---|
| 浏览器内置 | ✅ | ❌ (需要安装) |
| Promise 支持 | ✅ | ✅ |
| 自动 JSON 解析 | ❌ (需要手动调用 .json()) | ✅ |
| HTTP 错误自动 reject | ❌ (需要手动检查) | ✅ |
| 请求超时设置 | ❌ (需要 AbortController) | ✅ |
| 拦截器 | ❌ | ✅ |
| 取消请求 | ✅ (AbortController) | ✅ |
| 进度监控 | ❌ (需要 ReadableStream) | ✅ |
| 浏览器兼容性 | 现代浏览器 | 所有主流浏览器 |
十二、浏览器兼容性
Fetch API 在现代浏览器中得到广泛支持,但在一些旧浏览器中可能需要 polyfill:
- Chrome 42+
- Firefox 39+
- Safari 10.1+
- Edge 14+
对于 IE 浏览器,需要使用 polyfill 如 whatwg-fetch。
十三、TypeScript 支持
13.1 类型定义
// api.types.ts
export interface User {
id: number;
name: string;
email: string;
}
export interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// api.client.ts
import { User, Post } from "./api.types";
class ApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async getUsers(): Promise<User[]> {
const response = await fetch(`${this.baseURL}/users`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
async getPost(id: number): Promise<Post> {
const response = await fetch(`${this.baseURL}/posts/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
async createPost(post: Omit<Post, "id">): Promise<Post> {
const response = await fetch(`${this.baseURL}/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(post),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
}
export default ApiClient;
// 使用示例
const api = new ApiClient("https://api.example.com");
api.getUsers().then(users => {
users.forEach(user => {
console.log(user.name); // TypeScript 会提示 user 的属性
});
});
13.2 响应类型推断
async function fetchWithType<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return (await response.json()) as T;
}
// 使用示例
interface User {
id: number;
name: string;
}
fetchWithType<User[]>("https://api.example.com/users").then(users => {
users.forEach(user => {
console.log(user.name); // 类型安全
});
});
十四、最佳实践
- 始终处理错误:使用
.catch()捕获网络错误,并检查response.ok处理 HTTP 错误 - 使用 async/await:使代码更简洁易读
- 设置合理的超时:使用 AbortController 实现请求超时
- 优化请求头:只发送必要的请求头
- 处理响应类型:根据实际需要选择合适的响应处理方法
- 使用 HTTPS:确保请求安全
- 考虑 CORS:了解并正确配置跨域资源共享
十五、性能优化
15.1 请求合并
// 请求合并工具
class RequestMerger {
constructor() {
this.pendingRequests = new Map();
}
async merge(key, fetchFn) {
// 如果已经有相同的请求在进行中,返回其 promise
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key);
}
// 创建新的请求 promise
const promise = fetchFn().finally(() => {
// 请求完成后移除
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, promise);
return promise;
}
}
// 使用示例
const merger = new RequestMerger();
// 合并相同的请求
async function fetchUser(id) {
const key = `user_${id}`;
return merger.merge(key, () =>
fetch(`https://api.example.com/users/${id}`).then(res => res.json())
);
}
// 多次调用但只会发送一次请求
fetchUser(1).then(user => console.log("User 1:", user));
fetchUser(1).then(user => console.log("User 1 (cached):", user));
fetchUser(1).then(user => console.log("User 1 (cached):", user));
15.2 预加载
// 预加载资源
function preloadResource(url) {
return fetch(url, {
method: "GET",
cache: "force-cache",
});
}
// 预加载可能需要的资源
function preloadResources() {
const resources = [
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3",
];
resources.forEach(url => {
preloadResource(url).catch(() => {
// 预加载失败不影响主流程
console.log(`Failed to preload ${url}`);
});
});
}
// 在应用启动时预加载
window.addEventListener("load", preloadResources);
十六、安全考虑
16.1 CSRF 防护
// 获取 CSRF token
async function getCsrfToken() {
const response = await fetch("https://api.example.com/csrf-token");
const data = await response.json();
return data.csrfToken;
}
// 发送带 CSRF token 的请求
async function postWithCsrf(url, data) {
const csrfToken = await getCsrfToken();
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify(data),
credentials: "include",
});
return response.json();
}
16.2 敏感数据处理
// 安全处理敏感数据
function secureFetch(url, options = {}) {
// 确保使用 HTTPS
if (!url.startsWith("https://")) {
console.warn("Warning: Using non-HTTPS URL");
}
// 移除请求中的敏感信息
const sanitizedOptions = { ...options };
if (sanitizedOptions.headers) {
const headers = { ...sanitizedOptions.headers };
// 可以在这里添加其他敏感信息的处理
sanitizedOptions.headers = headers;
}
// 发送请求
return fetch(url, sanitizedOptions);
}
十七、与其他 API 的集成
17.1 Service Worker 集成
// service-worker.js
self.addEventListener("fetch", event => {
const url = new URL(event.request.url);
// 拦截 API 请求
if (url.pathname.startsWith("/api/")) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// 如果有缓存,返回缓存
if (cachedResponse) {
return cachedResponse;
}
// 否则发送网络请求
return fetch(event.request)
.then(response => {
// 缓存响应
const responseToCache = response.clone();
caches.open("api-cache").then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// 网络错误时的 fallback
return new Response(JSON.stringify({ error: "Network error" }), {
headers: { "Content-Type": "application/json" },
});
});
})
);
}
});
十八、测试方法
18.1 单元测试
// api.test.js
import { fetchData } from "./api";
// 模拟 fetch
global.fetch = jest.fn();
describe("API tests", () => {
beforeEach(() => {
fetch.mockClear();
});
test("should fetch data successfully", async () => {
const mockData = { id: 1, name: "Test" };
fetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockData),
});
const data = await fetchData("https://api.example.com/data");
expect(data).toEqual(mockData);
expect(fetch).toHaveBeenCalledWith("https://api.example.com/data");
});
test("should handle HTTP error", async () => {
fetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
});
await expect(fetchData("https://api.example.com/data")).rejects.toThrow(
"HTTP error! status: 404"
);
});
test("should handle network error", async () => {
fetch.mockRejectedValue(new Error("Network error"));
await expect(fetchData("https://api.example.com/data")).rejects.toThrow("Network error");
});
});
18.2 模拟响应
// mock-fetch.js
function createMockResponse(data, options = {}) {
return {
ok: options.ok !== false,
status: options.status || 200,
statusText: options.statusText || "OK",
headers: options.headers || {},
json: () => Promise.resolve(data),
text: () => Promise.resolve(typeof data === "string" ? data : JSON.stringify(data)),
blob: () => Promise.resolve(new Blob([JSON.stringify(data)])),
formData: () => {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value);
});
return Promise.resolve(formData);
},
};
}
// 使用示例
global.fetch = jest.fn().mockResolvedValue(createMockResponse({ id: 1, name: "Test" }));
// 测试错误响应
global.fetch = jest
.fn()
.mockResolvedValue(createMockResponse({ error: "Not found" }, { ok: false, status: 404 }));
十九、浏览器扩展和 polyfill
19.1 推荐的 polyfill
| Polyfill | 描述 | 适用场景 |
|---|---|---|
whatwg-fetch |
标准 Fetch API polyfill | 旧浏览器兼容 |
isomorphic-fetch |
同构 Fetch(浏览器和 Node.js) | 服务端渲染 |
cross-fetch |
跨平台 Fetch 实现 | 多环境兼容 |
19.2 Polyfill 的使用方法
# 安装
npm install whatwg-fetch
# 或
yarn add whatwg-fetch
// 在应用入口处引入
import "whatwg-fetch";
// 现在可以在所有浏览器中使用 fetch
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data));
19.3 特性检测
function supportsFetch() {
return "fetch" in window && typeof window.fetch === "function";
}
if (supportsFetch()) {
// 使用原生 fetch
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data));
} else {
// 使用替代方案(如 XMLHttpRequest)
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
}
二十、总结
Fetch API 是现代 JavaScript 中处理网络请求的强大工具,它提供了简洁、灵活的 API 来执行 HTTP 请求。虽然它在某些方面需要手动处理(如错误处理、JSON 解析),但它的基于 Promise 的设计使得代码更加清晰和易于维护。
对于复杂的应用场景,Axios 等第三方库可能提供更多便利功能,但对于大多数日常网络请求,原生的 Fetch API 已经足够强大和灵活。
随着浏览器对 Fetch API 支持的不断完善,它已经成为前端网络请求的首选方案之一。
二十一、实际项目中的应用(完整示例)
21.1 完整 API 封装
// api.js
class API {
constructor() {
this.baseURL =
process.env.NODE_ENV === "production"
? "https://api.example.com"
: "http://localhost:3000/api";
this.token = localStorage.getItem("token");
}
setToken(token) {
this.token = token;
localStorage.setItem("token", token);
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
"Content-Type": "application/json",
...options.headers,
};
if (this.token) {
headers["Authorization"] = `Bearer ${this.token}`;
}
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
if (response.status === 401) {
// 处理 token 过期
this.token = null;
localStorage.removeItem("token");
window.location.href = "/login";
}
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// CRUD 操作
get(endpoint, params = {}) {
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join("&");
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: "GET" });
}
post(endpoint, data) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
delete(endpoint) {
return this.request(endpoint, { method: "DELETE" });
}
}
// 导出单例
export default new API();
// 使用示例
import api from "./api";
// 登录
api.post("/auth/login", { email: "user@example.com", password: "password" }).then(response => {
api.setToken(response.token);
console.log("Login successful");
});
// 获取用户数据
api.get("/users").then(users => console.log("Users:", users));
21.2 请求拦截器实现
// 实现简单的请求拦截器
class FetchInterceptor {
constructor() {
this.requestInterceptors = [];
this.responseInterceptors = [];
}
useRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
}
useResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
}
async fetch(url, options = {}) {
// 应用请求拦截器
let config = { url, options };
for (const interceptor of this.requestInterceptors) {
config = await interceptor(config);
}
// 发送请求
let response = await fetch(config.url, config.options);
// 应用响应拦截器
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
return response;
}
}
// 使用示例
const interceptor = new FetchInterceptor();
// 添加请求拦截器
interceptor.useRequestInterceptor(async config => {
// 添加认证 token
const token = localStorage.getItem("token");
if (token) {
config.options.headers = {
...config.options.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
});
// 添加响应拦截器
interceptor.useResponseInterceptor(async response => {
// 统一处理错误
if (!response.ok) {
const error = new Error(`HTTP error! status: ${response.status}`);
error.status = response.status;
throw error;
}
return response;
});
// 使用拦截器发送请求
interceptor
.fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
21.3 文件上传(带进度监控)
// 完整的文件上传案例
async function uploadFile(file, onProgress) {
const formData = new FormData();
formData.append("file", file);
formData.append("fileName", file.name);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 进度监控
xhr.upload.addEventListener("progress", event => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
if (onProgress) {
onProgress(progress);
}
}
});
// 完成处理
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (error) {
reject(new Error("Invalid response format"));
}
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
// 错误处理
xhr.addEventListener("error", () => {
reject(new Error("Network error during upload"));
});
// 发送请求
xhr.open("POST", "https://api.example.com/upload");
xhr.send(formData);
});
}
// 使用示例
const fileInput = document.querySelector('input[type="file"]');
const progressBar = document.querySelector(".progress-bar");
fileInput.addEventListener("change", async e => {
const file = e.target.files[0];
if (file) {
try {
const result = await uploadFile(file, progress => {
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress}%`;
});
console.log("Upload complete:", result);
} catch (error) {
console.error("Error:", error);
}
}
});
21.4 分页加载实现
// 分页加载数据
class Pagination {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.page = 1;
this.pageSize = 10;
this.isLoading = false;
this.hasMore = true;
}
async loadMore() {
if (this.isLoading || !this.hasMore) {
return [];
}
this.isLoading = true;
try {
const response = await fetch(
`${this.apiUrl}?page=${this.page}&pageSize=${this.pageSize}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const items = data.items || [];
// 检查是否还有更多数据
this.hasMore = items.length === this.pageSize;
this.page++;
return items;
} catch (error) {
console.error("Error loading data:", error);
throw error;
} finally {
this.isLoading = false;
}
}
}
// 使用示例
const pagination = new Pagination("https://api.example.com/posts");
// 滚动加载
window.addEventListener("scroll", async () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
try {
const items = await pagination.loadMore();
if (items.length > 0) {
// 渲染新数据
renderItems(items);
}
} catch (error) {
console.error("Error:", error);
}
}
});
function renderItems(items) {
const container = document.querySelector(".items-container");
items.forEach(item => {
const element = document.createElement("div");
element.textContent = item.title;
container.appendChild(element);
});
}