pin_drop当前位置:知识文库 ❯ 图文
后端通信:19. 错误处理与重试 - 从入门到实践详解
教程简介
在网络请求中,错误是不可避免的——网络波动、服务器故障、请求超时、数据验证失败等各种情况都可能导致请求失败。如何优雅地处理这些错误,并在适当的情况下自动重试,是构建健壮前端应用的关键能力。本教程将全面讲解HTTP请求中的错误类型、错误处理策略、重试机制设计、断路器模式、降级方案等内容,帮助你构建高可用的前端应用。
核心概念
错误类型分类
重试策略对比
断路器模式
代码示例
关闭状态(CLOSED)→ 请求正常通过
失败率超过阈值
↓
打开状态(OPEN)→ 请求直接失败(快速失败)
超过冷却时间
↓
半开状态(HALF-OPEN)→ 允许少量请求通过
成功 → 回到关闭状态
失败 → 回到打开状态语法与用法
Fetch API错误处理
代码示例
// Fetch API的错误处理特点:
// 1. 网络错误会reject Promise
// 2. HTTP错误(4xx/5xx)不会reject,需要手动检查response.ok
try {
const response = await fetch('/api/data');
// HTTP错误不会抛出异常!
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
} catch (error) {
// 这里捕获:网络错误 或 手动抛出的HTTP错误
if (error instanceof TypeError) {
console.error('网络错误:', error.message);
} else {
console.error('请求错误:', error.message);
}
}XHR错误处理
代码示例
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(JSON.parse(xhr.responseText));
} else {
console.error(`HTTP ${xhr.status}`);
}
};
xhr.onerror = function() {
console.error('网络错误');
};
xhr.ontimeout = function() {
console.error('请求超时');
};
xhr.timeout = 10000; // 10秒超时
xhr.send();代码示例
示例1:错误类型与处理
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误类型与处理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f23;
color: #e0e0e0;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #48dbfb; margin-bottom: 30px; }
.card {
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
}
.card h2 { font-size: 16px; color: #feca57; margin-bottom: 16px; }
.error-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
}
.error-card {
background: #0f3460;
border-radius: 10px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.error-card:hover { transform: translateY(-2px); background: #162d50; }
.error-card h3 { font-size: 14px; margin-bottom: 8px; }
.error-card p { font-size: 12px; color: #a6adc8; line-height: 1.6; }
.status-code {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
}
.code-4xx { background: #f39c1233; color: #f39c12; }
.code-5xx { background: #e74c3c33; color: #e74c3c; }
.code-network { background: #9b59b633; color: #9b59b6; }
.code-cors { background: #e67e2233; color: #e67e22; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #48dbfb; color: #0f0f23; }
.btn-danger { background: #ff6b6b; color: white; }
.result-panel {
background: #0a0a1a;
border-radius: 8px;
padding: 16px;
margin-top: 12px;
font-family: monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.result-success { border-left: 4px solid #2ecc71; }
.result-error { border-left: 4px solid #ff6b6b; }
.code-block {
background: #0a0a1a;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>HTTP 请求错误类型与处理</h1>
<!-- 错误类型卡片 -->
<div class="card">
<h2>常见错误类型</h2>
<div class="error-grid">
<div class="error-card" onclick="simulateError('400')">
<span class="status-code code-4xx">400</span>
<h3 style="color:#f39c12;">Bad Request</h3>
<p>请求参数错误或格式不正确。不可重试,需修正请求。</p>
</div>
<div class="error-card" onclick="simulateError('401')">
<span class="status-code code-4xx">401</span>
<h3 style="color:#f39c12;">Unauthorized</h3>
<p>未认证或Token过期。需刷新Token或重新登录。</p>
</div>
<div class="error-card" onclick="simulateError('403')">
<span class="status-code code-4xx">403</span>
<h3 style="color:#f39c12;">Forbidden</h3>
<p>已认证但无权限。不可重试,需申请权限。</p>
</div>
<div class="error-card" onclick="simulateError('404')">
<span class="status-code code-4xx">404</span>
<h3 style="color:#f39c12;">Not Found</h3>
<p>资源不存在。不可重试,检查URL是否正确。</p>
</div>
<div class="error-card" onclick="simulateError('408')">
<span class="status-code code-5xx">408</span>
<h3 style="color:#e74c3c;">Request Timeout</h3>
<p>请求超时。可重试,考虑增加超时时间。</p>
</div>
<div class="error-card" onclick="simulateError('429')">
<span class="status-code code-4xx">429</span>
<h3 style="color:#f39c12;">Too Many Requests</h3>
<p>请求频率过高。需等待后重试,遵循Retry-After头。</p>
</div>
<div class="error-card" onclick="simulateError('500')">
<span class="status-code code-5xx">500</span>
<h3 style="color:#e74c3c;">Internal Server Error</h3>
<p>服务器内部错误。可重试,可能是临时故障。</p>
</div>
<div class="error-card" onclick="simulateError('502')">
<span class="status-code code-5xx">502</span>
<h3 style="color:#e74c3c;">Bad Gateway</h3>
<p>网关错误。可重试,上游服务可能暂时不可用。</p>
</div>
<div class="error-card" onclick="simulateError('503')">
<span class="status-code code-5xx">503</span>
<h3 style="color:#e74c3c;">Service Unavailable</h3>
<p>服务不可用。可重试,服务器过载或维护中。</p>
</div>
<div class="error-card" onclick="simulateError('network')">
<span class="status-code code-network">ERR</span>
<h3 style="color:#9b59b6;">Network Error</h3>
<p>网络连接失败。可重试,检查网络连接。</p>
</div>
<div class="error-card" onclick="simulateError('timeout')">
<span class="status-code code-network">TIMEOUT</span>
<h3 style="color:#9b59b6;">Request Timeout</h3>
<p>请求超时。可重试,考虑优化请求或增加超时。</p>
</div>
<div class="error-card" onclick="simulateError('cors')">
<span class="status-code code-cors">CORS</span>
<h3 style="color:#e67e22;">CORS Error</h3>
<p>跨域请求被拦截。不可重试,需配置CORS。</p>
</div>
</div>
</div>
<!-- 错误处理代码 -->
<div class="card">
<h2>统一错误处理</h2>
<div class="code-block">/**
* 自定义错误类
*/
class HttpError extends Error {
constructor(status, statusText, data) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.data = data;
this.isRetryable = this._isRetryable(status);
}
_isRetryable(status) {
// 5xx和408、429可重试
return status >= 500 || status === 408 || status === 429;
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
this.isRetryable = true;
}
}
class TimeoutError extends Error {
constructor(timeout) {
super(`请求超时 (${timeout}ms)`);
this.name = 'TimeoutError';
this.timeout = timeout;
this.isRetryable = true;
}
}
/**
* 统一错误处理函数
*/
function handleRequestError(error) {
if (error instanceof HttpError) {
switch (error.status) {
case 400:
showToast('请求参数错误,请检查输入', 'warning');
break;
case 401:
// Token过期,尝试刷新
redirectToLogin();
break;
case 403:
showToast('没有操作权限', 'error');
break;
case 404:
showToast('请求的资源不存在', 'error');
break;
case 429:
showToast('请求过于频繁,请稍后再试', 'warning');
break;
case 500:
case 502:
case 503:
showToast('服务器暂时不可用,请稍后再试', 'error');
break;
default:
showToast(`请求失败: ${error.message}`, 'error');
}
} else if (error instanceof NetworkError) {
showToast('网络连接失败,请检查网络', 'error');
} else if (error instanceof TimeoutError) {
showToast('请求超时,请稍后重试', 'warning');
} else {
showToast('未知错误', 'error');
}
}</div>
</div>
<!-- 模拟测试 -->
<div class="card">
<h2>错误模拟测试</h2>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="simulateError('200')">成功请求(200)</button>
<button class="btn btn-danger" onclick="simulateError('500')">服务器错误(500)</button>
<button class="btn btn-danger" onclick="simulateError('404')">资源不存在(404)</button>
<button class="btn btn-danger" onclick="simulateError('timeout')">模拟超时</button>
</div>
<div class="result-panel" id="errorTestResult">点击按钮模拟不同类型的错误</div>
</div>
</div>
<script>
async function simulateError(type) {
const result = document.getElementById('errorTestResult');
result.className = 'result-panel';
try {
let url;
switch (type) {
case '200': url = 'https://httpbin.org/get'; break;
case '400': url = 'https://httpbin.org/status/400'; break;
case '401': url = 'https://httpbin.org/status/401'; break;
case '403': url = 'https://httpbin.org/status/403'; break;
case '404': url = 'https://httpbin.org/status/404'; break;
case '408': url = 'https://httpbin.org/status/408'; break;
case '429': url = 'https://httpbin.org/status/429'; break;
case '500': url = 'https://httpbin.org/status/500'; break;
case '502': url = 'https://httpbin.org/status/502'; break;
case '503': url = 'https://httpbin.org/status/503'; break;
case 'network': throw new TypeError('Failed to fetch');
case 'timeout': throw new DOMException('The operation was aborted', 'AbortError');
case 'cors': url = 'https://restricted.example.com/api'; break;
default: url = 'https://httpbin.org/get';
}
const controller = new AbortController();
const timeoutId = type === 'timeout'
? setTimeout(() => controller.abort(), 10)
: null;
const response = await fetch(url, { signal: controller.signal });
if (timeoutId) clearTimeout(timeoutId);
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
const data = await response.json();
result.className = 'result-panel result-success';
result.textContent = `请求成功!\n\n状态码: ${response.status}\n数据: ${JSON.stringify(data, null, 2)}`;
} catch (error) {
result.className = 'result-panel result-error';
let errorInfo = `错误类型: ${error.name}\n错误信息: ${error.message}\n\n`;
if (error instanceof HttpError) {
errorInfo += `HTTP状态码: ${error.status}\n`;
errorInfo += `是否可重试: ${error.isRetryable ? '是' : '否'}\n\n`;
errorInfo += `处理建议:\n`;
switch (error.status) {
case 400: errorInfo += '- 检查请求参数是否正确\n- 验证数据格式'; break;
case 401: errorInfo += '- Token可能已过期\n- 尝试刷新Token\n- 跳转到登录页'; break;
case 403: errorInfo += '- 检查用户权限\n- 联系管理员'; break;
case 404: errorInfo += '- 检查URL是否正确\n- 资源可能已被删除'; break;
case 429: errorInfo += '- 降低请求频率\n- 检查Retry-After响应头\n- 实现请求限流'; break;
case 500: case 502: case 503:
errorInfo += '- 服务器临时故障\n- 可以重试(指数退避)\n- 显示友好错误页面'; break;
}
} else if (error.name === 'AbortError') {
errorInfo += `超时错误\n是否可重试: 是\n\n处理建议:\n- 增加超时时间\n- 重试请求\n- 检查网络连接`;
} else if (error instanceof TypeError) {
errorInfo += `网络错误\n是否可重试: 是\n\n处理建议:\n- 检查网络连接\n- 重试请求\n- 显示离线提示`;
} else {
errorInfo += `未知错误\n\n处理建议:\n- 记录错误日志\n- 显示通用错误提示`;
}
result.textContent = errorInfo;
}
}
function HttpError(status, statusText, data) {
this.name = 'HttpError';
this.message = `HTTP ${status}: ${statusText}`;
this.status = status;
this.statusText = statusText;
this.data = data;
this.isRetryable = status >= 500 || status === 408 || status === 429;
}
HttpError.prototype = Object.create(Error.prototype);
</script>
</body>
</html>示例2:重试机制实现
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重试机制实现</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1e1e2e;
color: #cdd6f4;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #89b4fa; margin-bottom: 30px; }
.card {
background: #313244;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
border: 1px solid #45475a;
}
.card h2 { font-size: 16px; color: #f9e2af; margin-bottom: 16px; }
.code-block {
background: #1e1e2e;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.demo-area {
background: #1e1e2e;
border-radius: 8px;
padding: 20px;
margin-top: 16px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #89b4fa; color: #1e1e2e; }
.btn-success { background: #a6e3a1; color: #1e1e2e; }
.btn-danger { background: #f38ba8; color: #1e1e2e; }
.btn-warning { background: #f9e2af; color: #1e1e2e; }
.retry-timeline {
margin-top: 16px;
}
.retry-step {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #45475a;
font-size: 13px;
}
.retry-step:last-child { border-bottom: none; }
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.step-pending { background: #45475a; color: #888; }
.step-active { background: #f9e2af; color: #1e1e2e; }
.step-success { background: #a6e3a1; color: #1e1e2e; }
.step-failed { background: #f38ba8; color: #1e1e2e; }
.step-waiting { background: #89b4fa; color: #1e1e2e; }
.log-area {
background: #1e1e2e;
border-radius: 8px;
padding: 16px;
font-family: monospace;
font-size: 12px;
color: #a6e3a1;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
margin-top: 12px;
}
.config-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
align-items: center;
}
.config-row label {
min-width: 100px;
font-size: 13px;
color: #a6adc8;
}
.config-row select, .config-row input {
padding: 8px 12px;
background: #45475a;
border: 1px solid #585b70;
border-radius: 6px;
color: #cdd6f4;
font-size: 14px;
outline: none;
}
</style>
</head>
<body>
<div class="container">
<h1>请求重试机制</h1>
<!-- 重试策略代码 -->
<div class="card">
<h2>指数退避重试实现</h2>
<div class="code-block">/**
* 带指数退避的重试函数
* @param {Function} fn - 要重试的异步函数
* @param {Object} options - 重试配置
* @returns {Promise} 最终结果或最后一次错误
*/
async function retryWithBackoff(fn, options = {}) {
const {
maxRetries = 3, // 最大重试次数
baseDelay = 1000, // 基础延迟(毫秒)
maxDelay = 30000, // 最大延迟
jitter = true, // 是否添加随机抖动
shouldRetry = () => true, // 自定义重试条件
onRetry = () => {}, // 重试回调
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await fn(attempt);
return result; // 成功,返回结果
} catch (error) {
lastError = error;
// 最后一次尝试,不再重试
if (attempt === maxRetries) break;
// 检查是否应该重试
if (!shouldRetry(error, attempt)) {
throw error; // 不可重试的错误,直接抛出
}
// 计算退避延迟
let delay = Math.min(
baseDelay * Math.pow(2, attempt), // 指数增长
maxDelay
);
// 添加随机抖动(避免重试风暴)
if (jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
// 处理429的Retry-After头
if (error.status === 429 && error.headers) {
const retryAfter = error.headers.get('Retry-After');
if (retryAfter) {
delay = Math.max(
parseInt(retryAfter) * 1000,
delay
);
}
}
// 通知重试
onRetry({
attempt: attempt + 1,
maxRetries,
delay: Math.round(delay),
error
});
// 等待
await sleep(delay);
}
}
throw lastError; // 所有重试都失败
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用示例
const data = await retryWithBackoff(
(attempt) => fetch('/api/data').then(r => {
if (!r.ok) throw new HttpError(r.status, r.statusText);
return r.json();
}),
{
maxRetries: 3,
baseDelay: 1000,
shouldRetry: (error) => {
// 只重试5xx和408
return error.status >= 500 || error.status === 408;
},
onRetry: ({ attempt, delay }) => {
console.log(`第${attempt}次重试,等待${delay}ms`);
}
}
);</div>
</div>
<!-- 交互式演示 -->
<div class="card">
<h2>重试机制交互演示</h2>
<div class="demo-area">
<div class="config-row">
<label>重试策略</label>
<select id="retryStrategy">
<option value="fixed">固定间隔</option>
<option value="exponential" selected>指数退避</option>
<option value="exponential-jitter">指数退避+抖动</option>
</select>
</div>
<div class="config-row">
<label>最大重试次数</label>
<select id="maxRetries">
<option value="1">1次</option>
<option value="3" selected>3次</option>
<option value="5">5次</option>
</select>
</div>
<div class="config-row">
<label>模拟失败次数</label>
<select id="failCount">
<option value="0">0次(立即成功)</option>
<option value="1">1次</option>
<option value="2" selected>2次</option>
<option value="3">3次</option>
<option value="5">5次(全部失败)</option>
</select>
</div>
<div style="display:flex; gap:8px; margin-top:8px;">
<button class="btn btn-primary" onclick="runRetryDemo()">开始重试演示</button>
<button class="btn btn-danger" onclick="resetDemo()">重置</button>
</div>
<div class="retry-timeline" id="retryTimeline"></div>
<div class="log-area" id="retryLog">等待开始...</div>
</div>
</div>
<!-- 断路器 -->
<div class="card">
<h2>断路器模式</h2>
<div class="code-block">/**
* 断路器
* 当失败率超过阈值时,快速失败而非继续请求
*/
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5; // 失败次数阈值
this.resetTimeout = options.resetTimeout || 30000; // 冷却时间(ms)
this.halfOpenMaxRequests = options.halfOpenMaxRequests || 3;
this.state = 'CLOSED'; // CLOSED | OPEN | HALF_OPEN
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
this.halfOpenRequests = 0;
}
async execute(fn) {
if (this.state === 'OPEN') {
// 检查是否可以进入半开状态
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
this.state = 'HALF_OPEN';
this.halfOpenRequests = 0;
} else {
throw new Error('断路器打开,快速失败');
}
}
if (this.state === 'HALF_OPEN') {
if (this.halfOpenRequests >= this.halfOpenMaxRequests) {
throw new Error('断路器半开,请求已满');
}
this.halfOpenRequests++;
}
try {
const result = await fn();
this._onSuccess();
return result;
} catch (error) {
this._onFailure();
throw error;
}
}
_onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED'; // 半开状态成功,关闭断路器
}
}
_onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN'; // 半开状态失败,重新打开
} else if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN'; // 失败次数超阈值,打开断路器
}
}
get info() {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime
};
}
}
// 使用示例
const circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000
});
try {
const data = await circuitBreaker.execute(async () => {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
} catch (error) {
if (error.message === '断路器打开,快速失败') {
// 显示降级内容
showFallbackContent();
}
}</div>
</div>
</div>
<script>
let demoRunning = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function runRetryDemo() {
if (demoRunning) return;
demoRunning = true;
const strategy = document.getElementById('retryStrategy').value;
const maxRetries = parseInt(document.getElementById('maxRetries').value);
const failCount = parseInt(document.getElementById('failCount').value);
const log = document.getElementById('retryLog');
const timeline = document.getElementById('retryTimeline');
log.textContent = '';
timeline.innerHTML = '';
let attemptCount = 0;
const startTime = Date.now();
log.textContent += `=== 重试演示开始 ===\n`;
log.textContent += `策略: ${strategy}\n`;
log.textContent += `最大重试: ${maxRetries}次\n`;
log.textContent += `模拟失败: ${failCount}次\n\n`;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
attemptCount++;
// 添加时间线步骤
const step = document.createElement('div');
step.className = 'retry-step';
step.id = `step-${attempt}`;
step.innerHTML = `
<div class="step-number step-active">${attempt + 1}</div>
<div style="flex:1;">
<div>第${attempt + 1}次请求</div>
<div id="step-detail-${attempt}" style="font-size:11px; color:#888;">请求中...</div>
</div>
`;
timeline.appendChild(step);
// 模拟请求
const isFail = attempt < failCount;
await sleep(500 + Math.random() * 500);
if (isFail) {
document.getElementById(`step-${attempt}`).querySelector('.step-number').className = 'step-number step-failed';
document.getElementById(`step-detail-${attempt}`).textContent = `失败 - 服务器错误(500)`;
log.textContent += `[${Date.now() - startTime}ms] 第${attempt + 1}次请求 → 失败\n`;
if (attempt < maxRetries) {
// 计算延迟
let delay;
switch (strategy) {
case 'fixed':
delay = 1000;
break;
case 'exponential':
delay = Math.min(1000 * Math.pow(2, attempt), 30000);
break;
case 'exponential-jitter':
delay = Math.min(1000 * Math.pow(2, attempt), 30000);
delay = delay * (0.5 + Math.random() * 0.5);
break;
}
delay = Math.round(delay);
// 等待步骤
const waitStep = document.createElement('div');
waitStep.className = 'retry-step';
waitStep.innerHTML = `
<div class="step-number step-waiting">↻</div>
<div style="flex:1;">
<div>等待 ${delay}ms 后重试</div>
<div style="font-size:11px; color:#888;">策略: ${strategy}</div>
</div>
`;
timeline.appendChild(waitStep);
log.textContent += `[${Date.now() - startTime}ms] 等待 ${delay}ms 后重试...\n`;
await sleep(Math.min(delay, 2000)); // 演示中最多等2秒
}
} else {
document.getElementById(`step-${attempt}`).querySelector('.step-number').className = 'step-number step-success';
document.getElementById(`step-detail-${attempt}`).textContent = `成功 - 200 OK`;
log.textContent += `[${Date.now() - startTime}ms] 第${attempt + 1}次请求 → 成功!\n`;
log.textContent += `\n总共尝试 ${attempt + 1} 次,总耗时 ${Date.now() - startTime}ms\n`;
demoRunning = false;
return;
}
}
log.textContent += `\n所有重试都失败,共尝试 ${attemptCount} 次\n`;
demoRunning = false;
}
function resetDemo() {
demoRunning = false;
document.getElementById('retryTimeline').innerHTML = '';
document.getElementById('retryLog').textContent = '等待开始...';
}
</script>
</body>
</html>示例3:完整的请求封装
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>完整的请求封装</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; }
.card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card h2 { font-size: 16px; color: #2c3e50; margin-bottom: 16px; }
.code-block {
background: #2c3e50;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>完整的请求封装</h1>
<div class="card">
<h2>生产级HTTP客户端</h2>
<div class="code-block">/**
* 生产级HTTP客户端
* 集成:超时控制、重试机制、断路器、请求/响应拦截器、错误处理
*/
class HttpClient {
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 10000;
this.defaultHeaders = config.headers || {};
// 重试配置
this.retryConfig = {
maxRetries: config.maxRetries || 2,
baseDelay: config.retryDelay || 1000,
retryOn: config.retryOn || [408, 429, 500, 502, 503, 504]
};
// 断路器
this.circuitBreaker = new CircuitBreaker({
failureThreshold: config.circuitThreshold || 5,
resetTimeout: config.circuitResetTimeout || 30000
});
// 拦截器
this.requestInterceptors = [];
this.responseInterceptors = [];
this.errorInterceptors = [];
}
// 添加请求拦截器
addRequestInterceptor(fn) {
this.requestInterceptors.push(fn);
return this;
}
// 添加响应拦截器
addResponseInterceptor(fn) {
this.responseInterceptors.push(fn);
return this;
}
// 添加错误拦截器
addErrorInterceptor(fn) {
this.errorInterceptors.push(fn);
return this;
}
// 核心请求方法
async request(method, url, data, options = {}) {
const config = {
method,
url: this.baseURL + url,
headers: { ...this.defaultHeaders, ...options.headers },
timeout: options.timeout || this.timeout,
data,
...options
};
// 执行请求拦截器
for (const interceptor of this.requestInterceptors) {
await interceptor(config);
}
// 通过断路器执行请求
return this.circuitBreaker.execute(async () => {
return this._executeWithRetry(config);
});
}
// 带重试的请求执行
async _executeWithRetry(config) {
const { maxRetries, baseDelay, retryOn } = this.retryConfig;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await this._fetch(config);
// 执行响应拦截器
let result = response;
for (const interceptor of this.responseInterceptors) {
result = await interceptor(result, config);
}
return result;
} catch (error) {
lastError = error;
// 最后一次尝试或不可重试的错误
if (attempt === maxRetries || !this._isRetryable(error, retryOn)) {
break;
}
// 计算延迟
const delay = this._calculateDelay(attempt, baseDelay, error);
// 等待后重试
await this._sleep(delay);
}
}
// 所有重试失败,执行错误拦截器
for (const interceptor of this.errorInterceptors) {
await interceptor(lastError, config);
}
throw lastError;
}
// 实际fetch请求(带超时)
async _fetch(config) {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
config.timeout
);
try {
const fetchOptions = {
method: config.method,
headers: config.headers,
signal: controller.signal
};
if (config.data && !['GET', 'HEAD'].includes(config.method)) {
if (config.headers['Content-Type'] === 'application/json') {
fetchOptions.body = JSON.stringify(config.data);
} else {
fetchOptions.body = config.data;
}
}
const response = await fetch(config.url, fetchOptions);
if (!response.ok) {
const error = new HttpError(
response.status,
response.statusText,
await this._safeParseResponse(response)
);
error.headers = response.headers;
throw error;
}
const data = await this._safeParseResponse(response);
return { data, status: response.status, headers: response.headers };
} catch (error) {
if (error.name === 'AbortError') {
throw new TimeoutError(config.timeout);
}
if (error instanceof TypeError) {
throw new NetworkError(error.message);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
_isRetryable(error, retryOn) {
if (error instanceof HttpError) {
return retryOn.includes(error.status);
}
if (error instanceof NetworkError || error instanceof TimeoutError) {
return true;
}
return false;
}
_calculateDelay(attempt, baseDelay, error) {
let delay = baseDelay * Math.pow(2, attempt);
// 处理429的Retry-After
if (error.status === 429 && error.headers) {
const retryAfter = error.headers.get('Retry-After');
if (retryAfter) {
delay = Math.max(parseInt(retryAfter) * 1000, delay);
}
}
// 添加抖动
delay = delay * (0.5 + Math.random() * 0.5);
return Math.round(Math.min(delay, 30000));
}
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async _safeParseResponse(response) {
try {
return await response.json();
} catch {
return null;
}
}
// 便捷方法
get(url, options) { return this.request('GET', url, null, options); }
post(url, data, options) { return this.request('POST', url, data, options); }
put(url, data, options) { return this.request('PUT', url, data, options); }
patch(url, data, options) { return this.request('PATCH', url, data, options); }
delete(url, options) { return this.request('DELETE', url, null, options); }
}
// ===== 使用示例 =====
const api = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 15000,
maxRetries: 3,
retryDelay: 1000,
headers: {
'Content-Type': 'application/json'
}
});
// 添加Token拦截器
api.addRequestInterceptor((config) => {
const token = getToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
});
// 添加401处理拦截器
api.addErrorInterceptor(async (error, config) => {
if (error.status === 401) {
const newToken = await refreshToken();
if (newToken) {
config.headers['Authorization'] = `Bearer ${newToken}`;
return api._fetch(config);
}
redirectToLogin();
}
});
// 添加日志拦截器
api.addResponseInterceptor((response, config) => {
console.log(`[API] ${config.method} ${config.url} → ${response.status}`);
return response;
});
// 发送请求
try {
const { data } = await api.get('/users');
console.log(data);
} catch (error) {
handleRequestError(error);
}</div>
</div>
<!-- 降级方案 -->
<div class="card">
<h2>降级方案</h2>
<div class="code-block">/**
* 降级策略
* 当主要请求失败时,提供备选方案
*/
// 1. 缓存降级:请求失败时返回缓存数据
async function fetchWithCacheFallback(url) {
try {
const response = await fetch(url);
const data = await response.json();
// 成功时更新缓存
localStorage.setItem(`cache:${url}`, JSON.stringify({
data, timestamp: Date.now()
}));
return { data, source: 'network' };
} catch (error) {
// 失败时尝试缓存
const cached = localStorage.getItem(`cache:${url}`);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
const age = Date.now() - timestamp;
return {
data,
source: 'cache',
age: Math.round(age / 1000) + '秒前',
warning: '数据可能不是最新的'
};
}
throw error;
}
}
// 2. 备用服务降级:主服务失败时请求备用服务
async function fetchWithFallback(primaryUrl, fallbackUrl) {
try {
return await fetch(primaryUrl).then(r => r.json());
} catch {
console.warn('主服务失败,尝试备用服务');
return await fetch(fallbackUrl).then(r => r.json());
}
}
// 3. 静态数据降级:所有请求失败时使用内置数据
const FALLBACK_DATA = {
users: [{ id: 1, name: '示例用户' }],
posts: [{ id: 1, title: '示例文章' }]
};
async function fetchWithStaticFallback(url) {
try {
return await fetch(url).then(r => r.json());
} catch {
const key = new URL(url).pathname.split('/').pop();
return FALLBACK_DATA[key] || null;
}
}
// 4. 骨架屏降级:加载失败时显示骨架屏
function showContentOrSkeleton(container, fetchFn) {
container.innerHTML = getSkeletonHTML();
fetchFn()
.then(data => {
container.innerHTML = renderContent(data);
})
.catch(() => {
container.innerHTML = getErrorHTML('加载失败,点击重试');
container.onclick = () => showContentOrSkeleton(container, fetchFn);
});
}</div>
</div>
</div>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 重试注意事项
代码示例
// 1. 不要重试非幂等请求
// GET、HEAD、OPTIONS、PUT、DELETE - 幂等,可重试
// POST - 非幂等,需谨慎重试
async function retryableRequest(method, url, options) {
const nonIdempotent = ['POST', 'PATCH'];
const shouldRetry = !nonIdempotent.includes(method.toUpperCase());
return retryWithBackoff(
() => fetch(url, options),
{
maxRetries: shouldRetry ? 3 : 0,
shouldRetry: (error) => {
if (!shouldRetry) return false;
return error.status >= 500 || error.status === 408;
}
}
);
}
// 2. 尊重Retry-After头
if (error.status === 429) {
const retryAfter = error.headers.get('Retry-After');
if (retryAfter) {
const delay = parseInt(retryAfter) * 1000;
await sleep(delay);
}
}
// 3. 设置合理的重试次数
// 网络错误:3-5次
// 服务端错误:2-3次
// 429错误:1次(等待Retry-After时间)
// 4. 添加重试上限,避免无限重试
const MAX_TOTAL_RETRY_TIME = 60000; // 1分钟2. 超时设置
代码示例
// 不同类型请求的超时建议
const TIMEOUT_PRESETS = {
quick: 3000, // 快速请求(如检查状态)
normal: 10000, // 普通请求(如获取列表)
slow: 30000, // 慢请求(如生成报告)
upload: 300000, // 文件上传(5分钟)
download: 600000 // 文件下载(10分钟)
};
// 带超时的fetch
function fetchWithTimeout(url, options = {}) {
const timeout = options.timeout || TIMEOUT_PRESETS.normal;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}3. 用户友好的错误提示
代码示例
// 根据错误类型显示不同的用户提示
function getUserFriendlyMessage(error) {
if (!navigator.onLine) {
return '网络连接已断开,请检查网络设置';
}
if (error instanceof TimeoutError) {
return '请求超时,请稍后重试';
}
if (error instanceof NetworkError) {
return '网络连接失败,请检查网络';
}
if (error instanceof HttpError) {
switch (error.status) {
case 400: return '请求参数有误,请检查输入';
case 401: return '登录已过期,请重新登录';
case 403: return '您没有权限执行此操作';
case 404: return '请求的内容不存在';
case 429: return '操作过于频繁,请稍后再试';
case 500: return '服务器开小差了,请稍后重试';
case 502: return '服务暂时不可用,请稍后重试';
case 503: return '服务维护中,请稍后访问';
default: return '操作失败,请稍后重试';
}
}
return '未知错误,请稍后重试';
}代码规范示例
规范的错误处理模块
代码示例
/**
* 错误处理模块
* 统一管理错误分类、处理策略和用户提示
*/
const ErrorHandler = {
// 错误分类
classify(error) {
if (error instanceof HttpError) {
if (error.status >= 400 && error.status < 500) return 'CLIENT_ERROR';
if (error.status >= 500) return 'SERVER_ERROR';
}
if (error instanceof NetworkError) return 'NETWORK_ERROR';
if (error instanceof TimeoutError) return 'TIMEOUT_ERROR';
return 'UNKNOWN_ERROR';
},
// 是否可重试
isRetryable(error) {
const category = this.classify(error);
return ['SERVER_ERROR', 'NETWORK_ERROR', 'TIMEOUT_ERROR'].includes(category);
},
// 是否需要重新认证
needsReauth(error) {
return error instanceof HttpError && error.status === 401;
},
// 获取用户提示
getUserMessage(error) {
return getUserFriendlyMessage(error);
},
// 获取处理建议
getAction(error) {
const category = this.classify(error);
const actions = {
CLIENT_ERROR: { type: 'fix', message: '请检查输入' },
SERVER_ERROR: { type: 'retry', message: '可以重试' },
NETWORK_ERROR: { type: 'retry', message: '检查网络后重试' },
TIMEOUT_ERROR: { type: 'retry', message: '稍后重试' },
UNKNOWN_ERROR: { type: 'report', message: '请联系客服' }
};
return actions[category];
},
// 全局错误处理
handle(error, context = {}) {
// 记录错误
this.log(error, context);
// 分类处理
if (this.needsReauth(error)) {
this.handleReauth();
return;
}
// 显示用户提示
const message = this.getUserMessage(error);
const action = this.getAction(error);
showToast(message, action.type === 'retry' ? 'warning' : 'error');
},
// 错误日志
log(error, context) {
const logEntry = {
timestamp: new Date().toISOString(),
type: error.name,
message: error.message,
status: error.status,
url: context.url,
method: context.method,
userAgent: navigator.userAgent
};
console.error('[ErrorHandler]', logEntry);
// 生产环境可发送到错误监控服务
// errorReportingService.send(logEntry);
},
handleReauth() {
// 清除Token,跳转登录
authModule.logout();
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
}
};常见问题与解决方案
问题1:重试导致重复操作
代码示例
// 问题:POST请求重试可能导致重复创建数据
// 解决方案1:使用幂等键
async function createOrder(orderData) {
const idempotencyKey = crypto.randomUUID();
return fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey // 服务端根据此键去重
},
body: JSON.stringify(orderData)
});
}
// 解决方案2:POST请求不自动重试
api.addErrorInterceptor((error, config) => {
if (config.method === 'POST' && error.status >= 500) {
// 提示用户手动重试
showRetryDialog();
return;
}
});问题2:并发请求全部失败
代码示例
// 问题:多个请求同时失败,同时重试导致重试风暴
// 解决方案:使用断路器 + 抖动
const circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000
});
// 抖动确保不同请求的重试时间不同
function calculateDelayWithJitter(attempt) {
const baseDelay = 1000 * Math.pow(2, attempt);
return baseDelay * (0.5 + Math.random() * 0.5);
}问题3:错误信息泄露敏感数据
代码示例
// 问题:错误信息中包含服务器内部信息
// 解决方案:前端不直接展示原始错误信息
function sanitizeErrorMessage(error) {
// 不展示原始错误信息
// 使用预定义的用户友好提示
const userMessage = getUserFriendlyMessage(error);
// 开发环境可以展示详细信息
if (process.env.NODE_ENV === 'development') {
console.error('详细错误:', error);
}
return userMessage;
}总结
错误处理与重试是构建高可用前端应用的关键能力,本教程涵盖了以下内容:
错误类型分类:理解网络错误、HTTP错误(4xx/5xx)、业务错误、CORS错误等不同类型的错误特征和处理方式。
错误处理策略:掌握自定义错误类、统一错误处理函数、用户友好提示等错误处理最佳实践。
重试机制:实现固定间隔、指数退避、指数退避+抖动等重试策略,理解不同策略的适用场景和优缺点。
断路器模式:学习断路器的CLOSED/OPEN/HALF_OPEN三种状态转换,防止级联失败和重试风暴。
完整请求封装:将超时控制、重试机制、断路器、拦截器集成为生产级HTTP客户端。
降级方案:掌握缓存降级、备用服务降级、静态数据降级、骨架屏降级等容错策略。
最佳实践:幂等请求重试、超时设置、用户提示、错误日志、敏感信息保护等生产环境必备知识。
常见问题
什么是19. 错误处理与重试?
19. 错误处理与重试是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习19. 错误处理与重试的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
19. 错误处理与重试有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
19. 错误处理与重试适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
19. 错误处理与重试的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
标签:
Promise
OAuth
响应码
async await
键盘输入
场景管理
JWT
请求缓存
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别