Promise封装Fetch + 重试机制(HTML+JS)

上图!!!

HTML源码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise封装Fetch + 重试机制</title>
    <link rel="stylesheet" href="./static/css/Promise封装Fetch.css">
</head>
<body>
    <div class="container">
        <h1>Promise 封装 Fetch + 重试机制</h1>
        <p class="subtitle">自动重试 · 超时中断 · JSON处理 · 状态日志</p>

        <div class="card">
            <h3>测试请求</h3>
            <div class="btn-group">
                <button class="btn btn-success" onclick="testSuccess()">成功请求 (JSONPlaceholder)</button>
                <button class="btn btn-danger" onclick="test404()">404 错误 (触发重试)</button>
                <button class="btn btn-warning" onclick="testTimeout()">超时模拟 (延迟接口)</button>
                <button class="btn btn-purple" onclick="testInvalidHost()">无效地址 (网络错误)</button>
            </div>
        </div>

        <div class="card">
            <h3>控制台输出</h3>
            <div id="console">
                <div class="log-line log-info">🚀 等待发起请求...</div>
            </div>
        </div>
    </div>

    <script>
        // ==============================
        // 核心: request 函数
        // ==============================

        /**
         * 基于 Fetch 封装的请求函数,支持自动重试、超时中断
         * @param {string} url       - 请求地址
         * @param {object} options   - fetch 配置项 (method, body, headers 等)
         * @param {number} retries   - 剩余重试次数 (内部递归使用)
         * @returns {Promise}        - 返回解析后的 JSON 数据
         */
        function request(url, options = {}, retries = 3) {
            // ---- [步骤1] 打印请求前时间戳 ----
            const startTime = Date.now();
            const timeStr = new Date(startTime).toLocaleTimeString();
            console.log(`%c[${timeStr}] ⏳ 发起请求: ${url}`, 'color: #00d4ff;');
            logToScreen(`[${timeStr}] ⏳ 发起请求: ${url}`, 'log-info');

            // ---- [步骤2] 合并默认 Headers,自动加 Content-Type ----
            const defaultHeaders = { 'Content-Type': 'application/json' };
            const mergedOptions = {
                ...options,
                headers: {
                    ...defaultHeaders,
                    ...options.headers,
                },
            };

            // ---- [步骤3] 超时控制: 使用 AbortController ----
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
            mergedOptions.signal = controller.signal;

            // ---- [步骤4] 发起 fetch 请求 ----
            return fetch(url, mergedOptions)
                .then(response => {
                    clearTimeout(timeoutId); // 清除超时定时器

                    const elapsed = Date.now() - startTime;
                    const endTime = new Date().toLocaleTimeString();
                    console.log(`%c[${endTime}] ✅ 收到响应: ${response.status} (耗时 ${elapsed}ms)`, 'color: #2ecc71;');
                    logToScreen(`[${endTime}] ✅ 收到响应: ${response.status} (耗时 ${elapsed}ms)`, 'log-success');

                    // ---- [步骤5] 状态码非 2xx 时 reject ----
                    if (!response.ok) {
                        // 主动抛出错误,进入 catch 链触发重试
                        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                    }

                    return response.json();
                })
                .catch(error => {
                    clearTimeout(timeoutId);

                    const elapsed = Date.now() - startTime;
                    const endTime = new Date().toLocaleTimeString();

                    // 判断是否是超时错误
                    if (error.name === 'AbortError') {
                        console.log(`%c[${endTime}] ⏰ 请求超时! (耗时 ${elapsed}ms)`, 'color: #f39c12;');
                        logToScreen(`[${endTime}] ⏰ 请求超时! (耗时 ${elapsed}ms)`, 'log-retry');
                        error = new Error('请求超时(超过5秒)');
                    } else {
                        console.log(`%c[${endTime}] ❌ 请求失败: ${error.message}`, 'color: #e74c3c;');
                        logToScreen(`[${endTime}] ❌ 请求失败: ${error.message}`, 'log-error');
                    }

                    // ---- [步骤6] 重试逻辑: 剩余重试次数 > 0 则递归重试 ----
                    if (retries > 0) {
                        const remaining = retries - 1;
                        console.log(`%c🔄 1秒后重试... (剩余 ${retries} 次)`, 'color: #f39c12;');
                        logToScreen(`🔄 1秒后重试... (剩余 ${retries} 次)`, 'log-retry');

                        // 延迟 1 秒后递归调用
                        return new Promise(resolve => {
                            setTimeout(() => {
                                resolve(request(url, options, remaining));
                            }, 1000);
                        });
                    }

                    // 重试耗尽,最终 reject
                    console.log(`%c💀 已重试3次,全部失败!`, 'color: #e74c3c; font-weight: bold;');
                    logToScreen(`💀 已重试3次,全部失败!`, 'log-error');
                    throw error;
                });
        }


        // ==============================
        // 页面交互: 控制台输出
        // ==============================

        function logToScreen(message, className) {
            const consoleDiv = document.getElementById('console');
            const line = document.createElement('div');
            line.className = `log-line ${className}`;
            line.textContent = message;
            consoleDiv.appendChild(line);
            consoleDiv.scrollTop = consoleDiv.scrollHeight;
        }


        // ==============================
        // async/await 调用示例
        // ==============================

        // 成功请求示例
        async function testSuccess() {
            try {
                console.clear();
                clearConsole();
                logToScreen('--- 测试: 成功请求 ---', 'log-info');

                // async/await 方式调用
                const data = await request('https://jsonplaceholder.typicode.com/posts/1');
                console.log('%c📦 返回数据:', 'color: #2ecc71; font-weight: bold;', data);
                logToScreen(`📦 返回数据: ${JSON.stringify(data, null, 2)}`, 'log-result');
            } catch (error) {
                logToScreen(`最终失败: ${error.message}`, 'log-error');
            }
        }

        // 404 错误示例 (触发重试)
        async function test404() {
            try {
                console.clear();
                clearConsole();
                logToScreen('--- 测试: 404 错误 ---', 'log-info');

                const data = await request('https://jsonplaceholder.typicode.com/posts/99999');
                logToScreen(`📦 返回数据: ${JSON.stringify(data)}`, 'log-result');
            } catch (error) {
                logToScreen(`最终失败: ${error.message}`, 'log-error');
            }
        }

        // 超时示例
        async function testTimeout() {
            try {
                console.clear();
                clearConsole();
                logToScreen('--- 测试: 超时请求 (延迟接口) ---', 'log-info');

                // 使用一个慢速接口来模拟超时
                const data = await request('https://httpbin.org/delay/6');
                logToScreen(`📦 返回数据: ${JSON.stringify(data)}`, 'log-result');
            } catch (error) {
                logToScreen(`最终失败: ${error.message}`, 'log-error');
            }
        }

        // 无效地址示例 (网络错误,触发重试)
        async function testInvalidHost() {
            try {
                console.clear();
                clearConsole();
                logToScreen('--- 测试: 无效地址 ---', 'log-info');

                const data = await request('https://this-domain-does-not-exist-12345.com/api');
                logToScreen(`📦 返回数据: ${JSON.stringify(data)}`, 'log-result');
            } catch (error) {
                logToScreen(`最终失败: ${error.message}`, 'log-error');
            }
        }

        // POST 请求示例 (展示自定义 headers 合并)
        async function testPost() {
            try {
                console.clear();
                clearConsole();
                logToScreen('--- 测试: POST 请求 ---', 'log-info');

                const data = await request('https://jsonplaceholder.typicode.com/posts', {
                    method: 'POST',
                    body: JSON.stringify({ title: 'foo', body: 'bar', userId: 1 }),
                    // Content-Type 会自动添加, 这里也可以覆盖
                });

                console.log('%c📦 POST 返回:', 'color: #2ecc71;', data);
                logToScreen(`📦 POST 返回: ${JSON.stringify(data, null, 2)}`, 'log-result');
            } catch (error) {
                logToScreen(`最终失败: ${error.message}`, 'log-error');
            }
        }

        function clearConsole() {
            document.getElementById('console').innerHTML = '';
        }
    </script>
</body>
</html>

CSS源码

css 复制代码
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
    min-height: 100vh;
    padding: 40px 20px;
    color: #e0e0e0;
}

.container {
    max-width: 800px;
    margin: 0 auto;
}

h1 {
    text-align: center;
    margin-bottom: 10px;
    font-size: 28px;
    background: linear-gradient(90deg, #00d4ff, #7b2ff7);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

.subtitle {
    text-align: center;
    color: #8892b0;
    margin-bottom: 30px;
    font-size: 14px;
}

.card {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 16px;
    padding: 24px;
    margin-bottom: 20px;
    backdrop-filter: blur(10px);
}

.card h3 {
    color: #00d4ff;
    margin-bottom: 16px;
    font-size: 16px;
}

.btn-group {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.btn {
    padding: 10px 20px;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    transition: all 0.3s ease;
}

.btn-success {
    background: linear-gradient(135deg, #00b4d8, #0077b6);
    color: #fff;
}

.btn-danger {
    background: linear-gradient(135deg, #e63946, #c1121f);
    color: #fff;
}

.btn-warning {
    background: linear-gradient(135deg, #f4a261, #e76f51);
    color: #fff;
}

.btn-purple {
    background: linear-gradient(135deg, #7b2ff7, #5a189a);
    color: #fff;
}

.btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}

.btn:active {
    transform: translateY(0);
}

#console {
    background: #0a0a1a;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 12px;
    padding: 16px;
    min-height: 300px;
    max-height: 500px;
    overflow-y: auto;
    font-family: 'Consolas', 'Courier New', monospace;
    font-size: 13px;
    line-height: 1.6;
}

.log-line {
    padding: 4px 8px;
    border-radius: 4px;
    margin: 2px 0;
    animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.log-info {
    color: #00d4ff;
}

.log-success {
    color: #2ecc71;
}

.log-error {
    color: #e74c3c;
}

.log-retry {
    color: #f39c12;
}

.log-time {
    color: #8892b0;
}

.log-result {
    background: rgba(46, 204, 113, 0.1);
    border-left: 3px solid #2ecc71;
    color: #a0ffc0;
}
相关推荐
cyadyx1 小时前
Vite 比 Webpack 构建效率更高
前端·webpack·node.js·vite
mCell3 小时前
AI 时代 SVG 画图的潜力
前端·agent·svg
我命由我123458 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
Hopebearer_9 小时前
页面突然只剩 DOM?一次静态资源版本错配排查
前端·部署
抱抱宝9 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
东风破_10 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
用户9385156350710 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
圣殿骑士-Khtangc11 小时前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
BigTopOne12 小时前
【ijkplayer】 硬解码流程
前端
kyriewen12 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek