15.2 fetch API 与 AbortController
fetch API 深度解析
基本用法与现代实践
// 基础GET请求
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error; // 重新抛出以便外部处理
}
}
// 高级POST请求
async function postData(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify(data),
credentials: 'include' // 包含cookies
});
if (!response.ok) {
const errorData = await response.json();
throw new ApiError(response.status, errorData.message);
}
return response.json();
}
请求配置选项详解
const fetchOptions = {
method: 'PUT', // GET, POST, PUT, DELETE, etc.
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({ key: 'value' }), // Blob, FormData, URLSearchParams等
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
signal: null // 用于取消请求的AbortSignal
};
响应处理高级技巧
流式处理大数据
async function processLargeData(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
console.log('Received chunk:', value.byteLength, 'bytes');
}
console.log('Complete data:', result);
return result;
}
进度监控
async function fetchWithProgress(url, onProgress) {
const response = await fetch(url);
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
const chunks = [];
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (contentLength) {
const percent = Math.round((receivedLength / contentLength) * 100);
onProgress(percent, receivedLength, contentLength);
}
}
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
return new TextDecoder().decode(chunksAll);
}
// 使用示例
fetchWithProgress('large-file.txt', (percent) => {
console.log(`Downloaded: ${percent}%`);
});
AbortController 实战应用
基本取消请求
// 创建控制器
const controller = new AbortController();
const { signal } = controller;
// 发起可取消的请求
fetch('some-api', { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Fetch error:', err);
}
});
// 取消请求
controller.abort();
超时自动取消
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
options.signal = signal;
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
return fetch(url, options)
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(err => {
clearTimeout(timeoutId);
throw err;
});
}
// 使用示例
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.error('Request timed out');
} else {
console.error('Error:', err);
}
});
多请求竞争与取消
async function fetchFastest(urls, timeout = 3000) {
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const promises = urls.map(url =>
fetch(url, { signal })
.then(response => {
clearTimeout(timeoutId);
return response.json();
})
);
return await Promise.any(promises);
} catch (err) {
if (err.name === 'AggregateError') {
throw new Error('All requests failed');
} else if (err.name === 'AbortError') {
throw new Error('Request timeout');
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
// 使用示例
fetchFastest([
'https://api1.example.com/data',
'https://api2.example.com/data',
'https://api3.example.com/data'
])
.then(data => console.log('Fastest response:', data))
.catch(err => console.error('Error:', err.message));
高级应用场景
可取消的并行请求
async function fetchMultiple(urls, { timeout = 5000 } = {}) {
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const requests = urls.map(url =>
fetch(url, { signal }).then(res => res.json())
);
const results = await Promise.all(requests);
clearTimeout(timeoutId);
return results;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('Request timeout exceeded');
}
throw err;
}
}
// 使用示例
fetchMultiple([
'https://api.example.com/users',
'https://api.example.com/posts'
])
.then(([users, posts]) => {
console.log('Users:', users);
console.log('Posts:', posts);
})
.catch(err => console.error('Error:', err.message));
重试机制实现
async function fetchWithRetry(url, options = {}, maxRetries = 3, retryDelay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
const controller = new AbortController();
options.signal = controller.signal;
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = new Error(`HTTP error! status: ${response.status}`);
error.status = response.status;
// 如果是服务器错误(5xx)才重试
if (response.status >= 500 && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
throw error;
}
return response.json();
} catch (error) {
lastError = error;
// 如果不是中止错误且不是最后一次尝试,则等待后重试
if (error.name !== 'AbortError' && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}
throw lastError;
}
// 使用示例
fetchWithRetry('https://api.example.com/unstable', {}, 3, 1000)
.then(data => console.log(data))
.catch(err => console.error('Failed after retries:', err));
错误处理与调试
自定义错误类
class FetchError extends Error {
constructor(message, { status, statusText, url } = {}) {
super(message);
this.name = 'FetchError';
this.status = status;
this.statusText = statusText;
this.url = url;
}
toString() {
return `${this.name}: ${this.message} (${this.status} ${this.statusText} at ${this.url})`;
}
}
async function fetchWithEnhancedError(url) {
const response = await fetch(url);
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (e) {
errorData = { message: response.statusText };
}
throw new FetchError(errorData.message || 'Request failed', {
status: response.status,
statusText: response.statusText,
url: response.url
});
}
return response.json();
}
网络状态检测
function checkNetworkStatus() {
if (!navigator.onLine) {
throw new Error('Offline: No network connection');
}
// 更精确的检测方式
return fetch('https://httpbin.org/get', {
method: 'HEAD',
cache: 'no-store',
mode: 'no-cors'
}).catch(() => {
throw new Error('Network may be unstable');
});
}
async function fetchWithNetworkCheck(url) {
await checkNetworkStatus();
return fetchWithRetry(url);
}
最佳实践总结
-
请求取消:
- 总是为长时间请求添加取消能力
- 页面切换时取消未完成请求
- 用户导航离开时清理资源
-
错误处理:
- 区分网络错误、服务器错误和应用错误
- 为错误提供足够上下文
- 实现适当的重试机制
-
性能优化:
- 使用流式处理大响应
- 实现进度反馈
- 合理设置超时
-
安全实践:
- 验证和清理所有API响应
- 使用CSP和CORs策略
- 保护敏感请求头
-
现代功能:
- 利用
AbortController进行请求控制 - 使用
Promise.any实现竞速请求 - 考虑使用
Streams API处理大数据
- 利用
实际应用示例
文件分片上传
async function uploadFile(file, url, onProgress) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const chunks = Math.ceil(file.size / CHUNK_SIZE);
const controller = new AbortController();
try {
for (let i = 0; i < chunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
formData.append('originalName', file.name);
await fetch(url, {
method: 'POST',
body: formData,
signal: controller.signal
});
const percent = Math.round(((i + 1) / chunks) * 100);
onProgress(percent);
}
return { success: true };
} catch (err) {
if (err.name !== 'AbortError') {
throw err;
}
return { success: false, aborted: true };
}
}
// 使用示例
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
uploadFile(file, '/api/upload', (percent) => {
console.log(`Upload progress: ${percent}%`);
})
.then(result => {
if (result.aborted) {
console.log('Upload was aborted');
} else {
console.log('Upload completed');
}
})
.catch(err => {
console.error('Upload failed:', err);
});
});
实时数据轮询
class DataPoller {
constructor(url, interval = 5000) {
this.url = url;
this.interval = interval;
this.controller = null;
this.isPolling = false;
}
start(callback) {
if (this.isPolling) return;
this.isPolling = true;
this.controller = new AbortController();
const poll = async () => {
if (!this.isPolling) return;
try {
const response = await fetch(this.url, {
signal: this.controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
callback(null, data);
} catch (err) {
if (err.name !== 'AbortError') {
callback(err);
}
} finally {
if (this.isPolling) {
this.timeoutId = setTimeout(poll, this.interval);
}
}
};
poll();
}
stop() {
if (!this.isPolling) return;
this.isPolling = false;
clearTimeout(this.timeoutId);
this.controller.abort();
}
}
// 使用示例
const poller = new DataPoller('https://api.example.com/updates', 3000);
poller.start((err, data) => {
if (err) {
console.error('Polling error:', err);
return;
}
console.log('New data:', data);
});
// 停止轮询
// poller.stop();
#前端开发
分享于 2025-03-25