一、Fetch 的本质:它到底是什么?
1.1 定义与定位
Fetch API 是浏览器提供的原生 JavaScript 接口,用于替代老旧的 XMLHttpRequest(XHR)。它基于 Promise 设计,提供了更强大、更灵活的网络请求能力。
// 最基础的用法
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
1.2 Fetch vs XHR:核心区别
| 特性 | Fetch | XMLHttpRequest |
|---|---|---|
| API 风格 | Promise(现代异步) | 回调事件(onload/onerror) |
| 语法简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 流式处理 | ✅ 支持 Stream API | ❌ 不支持 |
| 请求取消 | ✅ AbortController | ✅ abort() |
| Cookie 携带 | 默认不带,需配置 | 默认带 |
| 错误处理 | 仅网络错误 reject,HTTP 错误不 reject | 所有错误都触发 onerror |
| 进度监控 | ❌ 不支持上传/下载进度 | ✅ onprogress |
| 超时设置 | ❌ 需借助 AbortController | ✅ timeout 属性 |
二、Fetch 的完整语法结构
fetch(input, init)
参数详解
1. input:请求地址
- 字符串 URL
Request对象(可复用配置)
2. init:配置对象(核心)
const init = {
method: 'GET', // 请求方法
headers: { // 请求头
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ // 请求体(GET/HEAD 不能有)
name: '张三'
}),
mode: 'cors', // 跨域模式:cors/no-cors/same-origin
credentials: 'include', // 是否携带凭证:omit/same-origin/include
cache: 'default', // 缓存策略:default/no-cache/reload/force-cache/only-if-cached
redirect: 'follow', // 重定向:follow/error/manual
referrer: 'client', // referrer 策略
referrerPolicy: 'strict-origin-when-cross-origin',
integrity: '', // 子资源完整性(SRI)
keepalive: false, // 页面卸载后是否保持请求
signal: abortController.signal // 取消信号
};
三、Response 对象
Fetch 返回的 Response 对象包含了完整的响应信息:
const response = await fetch('/api/data');
// 响应属性
console.log(response.status); // 200
console.log(response.statusText); // 'OK'
console.log(response.ok); // true(status 200-299)
console.log(response.headers); // Headers 对象
console.log(response.url); // 最终请求 URL
console.log(response.redirected); // 是否发生过重定向
console.log(response.type); // basic/cors/opaque/opaqueredirect
// 响应体解析方法(只能调用一次!)
response.text() // 返回字符串
response.json() // 返回 JSON 对象
response.blob() // 返回 Blob(文件/图片)
response.arrayBuffer() // 返回 ArrayBuffer
response.formData() // 返回 FormData
response.clone() // 克隆 Response(可多次读取 body)
重要:Fetch 不抛出 HTTP 错误
// 错误示例:404 不会被 catch 捕获
fetch('/api/notfound')
.then(response => {
console.log('进入 then,而不是 catch');
return response.json();
})
.catch(err => {
console.log('这里不会执行!');
});
// ✅ 正确做法:手动检查 response.ok
fetch('/api/notfound')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.catch(err => {
console.error('请求失败:', err);
});
四、Fetch 的高级特性
4.1 流式处理(Streaming)
Fetch 支持流式读取,适合大文件下载或实时数据:
// 流式下载大文件
async function downloadLargeFile(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
console.log(`下载进度: ${(receivedLength / contentLength * 100).toFixed(2)}%`);
}
// 合并所有 chunks
const blob = new Blob(chunks);
return blob;
}
4.2 取消请求(AbortController)
// 创建取消控制器
const controller = new AbortController();
const { signal } = controller;
// 发起请求
fetch('/api/slow-request', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已被取消');
} else {
console.error('其他错误:', err);
}
});
// 取消请求(例如:用户点击取消按钮)
document.querySelector('#cancelBtn').addEventListener('click', () => {
controller.abort();
});
// 设置超时(结合 AbortController)
function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
4.3 请求重试机制
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response;
} catch (error) {
lastError = error;
console.log(`第 ${i + 1} 次尝试失败,${maxRetries - i - 1} 次重试剩余`);
// 指数退避:等待 2^i 秒
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
throw lastError;
}
// 使用
fetchWithRetry('/api/unstable-endpoint', { method: 'GET' }, 5)
.then(res => res.json())
.then(data => console.log('成功:', data))
.catch(err => console.error('全部重试失败:', err));
4.4 并发请求与控制
// 1. 并行请求(Promise.all)
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
// 2. 竞速请求(Promise.race)- 获取最快响应的 CDN
const cdnUrls = [
'https://cdn1.example.com/data.json',
'https://cdn2.example.com/data.json',
'https://cdn3.example.com/data.json'
];
const fastest = await Promise.race(
cdnUrls.map(url => fetch(url).then(r => r.json()))
);
console.log('最快返回的数据:', fastest);
// 3. 控制并发数(限制同时请求数量)
async function concurrentFetch(urls, limit = 3) {
const results = [];
const executing = [];
for (const url of urls) {
const promise = fetch(url).then(r => r.json());
results.push(promise);
if (limit <= urls.length) {
const e = promise.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
4.5 流式上传(Streaming Request Body)
// 使用流上传大文件(无需完全加载到内存)
async function uploadLargeFile(file) {
const stream = file.stream(); // File 对象支持流
const reader = stream.getReader();
// 创建可读流
const readableStream = new ReadableStream({
start(controller) {
return pump();
function pump() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
return pump();
});
}
}
});
const response = await fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
},
body: readableStream,
duplex: 'half' // 必须设置
});
return response.json();
}
五、Fetch 的配置详解
5.1 credentials:控制 Cookie 携带
// 默认:不发送 cookie(跨域时不发,同源才发)
fetch('/api/data', { credentials: 'same-origin' });
// 跨域请求携带 cookie(需要后端支持 CORS)
fetch('https://other-domain.com/api', {
credentials: 'include' // ✅ 跨域也发送
});
// 完全不发送 cookie
fetch('/api/data', { credentials: 'omit' });
5.2 mode:跨域模式
// cors:默认,允许跨域请求
fetch('/api/data', { mode: 'cors' });
// no-cors:只允许简单请求(不能读取响应内容)
fetch('/api/data', { mode: 'no-cors' })
.then(res => {
console.log(res.type); // 'opaque'
console.log(res.status); // 0
// ❌ 无法读取响应体
});
// same-origin:只允许同源请求
fetch('https://other-domain.com/api', { mode: 'same-origin' }); // 报错
5.3 cache:缓存策略
// default:根据 HTTP 缓存头决定
fetch('/api/data', { cache: 'default' });
// no-cache:每次都验证缓存(ETag)
fetch('/api/data', { cache: 'no-cache' });
// reload:强制从服务器获取(忽略缓存)
fetch('/api/data', { cache: 'reload' });
// force-cache:强制使用缓存(即使过期)
fetch('/api/data', { cache: 'force-cache' });
// only-if-cached:只使用缓存(无缓存则报错)
fetch('/api/data', { cache: 'only-if-cached' });
5.4 redirect:重定向处理
// follow:自动跟随重定向(默认)
fetch('/api/redirect', { redirect: 'follow' });
// error:遇到重定向就报错
fetch('/api/redirect', { redirect: 'error' });
// manual:手动处理重定向
fetch('/api/redirect', { redirect: 'manual' })
.then(res => {
if (res.type === 'opaqueredirect') {
// 手动跳转
window.location.href = res.url;
}
});
5.5 referrerPolicy:来源控制
// 所有场景都发送完整 referrer
fetch('/api/data', { referrerPolicy: 'unsafe-url' });
// 同源发送完整,跨域只发送源
fetch('/api/data', { referrerPolicy: 'strict-origin-when-cross-origin' });
// 完全不发送 referrer
fetch('/api/data', { referrerPolicy: 'no-referrer' });
5.6 integrity:子资源完整性(SRI)
// 验证资源哈希,防止 CDN 被篡改
fetch('https://cdn.example.com/library.js', {
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'
})
.then(res => res.text())
.then(script => eval(script)); // 仅当哈希匹配才执行
六、Fetch 的实战技巧与踩坑
坑1:跨域请求携带 Cookie 失效
// ❌ 错误:只设置了 credentials,服务端也要配置
fetch('https://api.example.com', {
credentials: 'include'
});
// ✅ 正确:服务端需要设置
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: 具体域名(不能是 *)
坑2:Response body 只能读取一次
const response = await fetch('/api/data');
const text = await response.text(); // 读取第一次
const json = await response.json(); // ❌ 报错:Body already read
// ✅ 解决方案:使用 clone()
const response = await fetch('/api/data');
const responseClone = response.clone();
const text = await response.text();
const json = await responseClone.json(); // 正常
坑3:JSON 解析错误处理
// ❌ 错误:直接假设返回 JSON
fetch('/api/data')
.then(res => res.json()) // 如果返回 HTML 或纯文本会报错
.catch(err => console.error('解析错误:', err));
// ✅ 正确:检查 Content-Type
async function safeFetch(url) {
const res = await fetch(url);
const contentType = res.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await res.json();
} else if (contentType?.includes('text/')) {
return await res.text();
} else {
return await res.blob();
}
}
坑4:GET 请求不能带 body
// ❌ 错误:GET 请求有 body(会被忽略)
fetch('/api/data', {
method: 'GET',
body: JSON.stringify({ id: 1 }) // 被忽略
});
// ✅ 正确:使用 query string
fetch('/api/data?id=1');
// 或使用 URLSearchParams
const params = new URLSearchParams({ id: 1 });
fetch(`/api/data?${params}`);
坑5:文件上传 FormData
// 上传单个文件
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', '张三');
fetch('/api/upload', {
method: 'POST',
body: formData // ❌ 不要设置 Content-Type,浏览器自动设置 boundary
});
// 上传多个文件
const files = fileInput.files;
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
七、性能优化与最佳实践
7.1 复用 Request 对象
// 创建可复用的 Request
const baseRequest = new Request('/api/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
}
});
// 多次使用
fetch(baseRequest)
fetch(baseRequest) // 每次都是独立的请求
7.2 使用 URLSearchParams 构建查询参数
const params = new URLSearchParams({
page: 1,
limit: 20,
sort: 'desc',
keyword: '搜索词'
});
fetch(`/api/search?${params}`);
7.3 预连接(Preconnect)
<!-- HTML 中提前建立连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
7.4 数据缓存策略(内存缓存)
// 简单的内存缓存
class CacheManager {
constructor() {
this.cache = new Map();
}
async fetch(url, options = {}) {
const key = `${url}_${JSON.stringify(options)}`;
if (this.cache.has(key)) {
console.log('从缓存返回');
return this.cache.get(key).clone();
}
const response = await fetch(url, options);
// 只缓存 GET 请求且状态成功
if (options.method !== 'GET' || !response.ok) {
return response;
}
// 克隆后缓存
const cloned = response.clone();
this.cache.set(key, cloned);
return response;
}
clear() {
this.cache.clear();
}
}
const cache = new CacheManager();