JavaScript Fetch方法详解


一、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() 函数接受两个参数:

  1. url:必需,请求的 URL 字符串
  2. 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); // 类型安全
    });
});

十四、最佳实践

  1. 始终处理错误:使用 .catch() 捕获网络错误,并检查 response.ok 处理 HTTP 错误
  2. 使用 async/await:使代码更简洁易读
  3. 设置合理的超时:使用 AbortController 实现请求超时
  4. 优化请求头:只发送必要的请求头
  5. 处理响应类型:根据实际需要选择合适的响应处理方法
  6. 使用 HTTPS:确保请求安全
  7. 考虑 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);
    });
}

参考资料


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
 本篇
JavaScript Fetch方法详解 JavaScript Fetch方法详解
深入讲解JavaScript Fetch API的使用方法、参数配置、错误处理以及与其他HTTP客户端的比较
2026-05-03
下一篇 
JavaScript十大设计模式详解 JavaScript十大设计模式详解
深入讲解JavaScript十大设计模式,包括工厂模式、单体模式、模块模式、代理模式、职责链模式、命令模式、模板方法模式、策略模式、发布订阅模式、中介者模式
  目录