Fetch API 是现代 JavaScript 中进行网络请求的核心方法。它提供了一个基于 Promise 的接口,用于替代老旧的 XMLHttpRequest,让开发者能以更简洁、更现代的方式处理 HTTP 请求与响应。
Fetch API 的核心是请求 (Request) 和响应 (Response) 这两个对象。使用时,你创建一个请求,fetch() 方法会返回一个 Promise,这个 Promise 最终会兑现为包含服务器返回数的 Response 对象。
下面从基础到进阶,详细拆解它的用法。
1. 基础语法
js
// 链式
fetch(resource, options)
.then(response => { ... })
.catch(error => { ... });
// async-await
const fn = async () => {
try {
const response = await fetch(resource, options);
if(!response.ok) throw new Error(...)
return await response.json();
} catch (error => {
...
});
}
fetch() 是全局函数,接受两个参数:
- resource :请求目标,通常是一个 URL 字符串或
Request对象。 - options:可选配置对象,包含方法、请求头、请求体等。
fetch() 返回一个 Promise,它在收到响应头时即 resolve (即使状态码是 404/500),只有在网络错误或请求被阻止时才会 reject。
2. 请求配置 (options)
第二个参数通常是一个对象,常用属性:
- method :
'GET'、'POST'、'PUT'、'DELETE'等(默认'GET')。 - headers : 请求头对象,常用
Headers实例或普通对象。 - body : 请求体,可以是字符串、
FormData、Blob、URLSearchParams等。 - mode :
'cors'、'no-cors'、'same-origin'。 - credentials :
'omit'(默认,不发送 cookie)、'same-origin'(同源发送)、'include'(跨域也发送)。 - cache : 缓存模式,如
'default'、'no-store'、'reload'等。 - redirect :
'follow'(默认,跟随重定向)、'error'、'manual'。 - signal : 传入
AbortController.signal,用于中止请求。
3. 常用响应解析方法
Response 对象提供了多种读取主体内容的方法,每个方法都返回 Promise,且只能调用一次(流已消耗)。
| 方法 | 用途 | 返回类型 |
|---|---|---|
response.json() |
解析 JSON 格式数据 | Promise (解析为 JavaScript 对象) |
response.text() |
解析纯文本格式数据 | Promise (解析为字符串) |
response.blob() |
处理图片、文件等二进制数据 | Promise (解析为 Blob 对象) |
response.formData() |
解析 FormData 格式响应 |
Promise (解析为 FormData 对象) |
response.arrayBuffer() |
处理底层二进制流 | Promise (解析为 ArrayBuffer) |
js
// 获取图片并显示
fetch('https://example.com/photo.jpg')
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
});
4. 发起简单 GET 请求
js
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // 解析 JSON 数据
})
.then(data => console.log('数据:', data))
.catch(error => console.error('请求失败:', error));
关键点:
Fetch API 一个常见陷阱是:fetch() 返回的 Promise 仅在网络错误或请求被阻止 时才会 reject。即使服务器返回 404 或 500 这样的错误状态码,fetch() 也仍然会 resolve。因此,必须手动检查 response.ok(当 HTTP 状态码在 200-299 范围内时为 true) 或 response.status 来处理服务器端错误
5. POST 请求示例
5.1 发送 JSON 数据
js
fetch('https://api.example.com/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 如果需要 JWT 认证
// 'Authorization': 'Bearer <token>'
},
body: JSON.stringify({
name: '张三',
age: 30
})
})
.then(res => res.json())
.then(data => console.log('创建成功:', data))
.catch(err => console.error(err));
5.2 发送表单数据 (FormData)
js
const formData = new FormData();
formData.append('username', 'lisi');
formData.append('avatar', fileInput.files[0]); // 上传文件
fetch('/upload', {
method: 'POST',
body: formData // 浏览器会自动设置 Content-Type 为 multipart/form-data
})
.then(res => res.text())
.then(console.log);
5.3 使用 URLSearchParams(类传统表单)
js
const params = new URLSearchParams({
key1: 'value1',
key2: 'value2'
});
fetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
6. 错误处理完整模式
js
// 示例一
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
// 尝试提取服务器返回的错误信息
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被中止');
} else if (error.message.includes('Failed to fetch')) {
console.log('网络连接失败或跨域问题');
} else {
console.error('请求出错:', error);
}
throw error; // 可继续向上抛出
}
}
// 示例二
async function fetchData(url, options = {}) {
try {
const response = await fetch(url, options);
// 1. 检查 HTTP 状态码
if (!response.ok) {
// 根据状态码进行更细致的处理
let errorMessage = `Request failed with status ${response.status}`;
if (response.status === 404) {
errorMessage = 'Resource not found';
} else if (response.status >= 500) {
errorMessage = 'Internal server error';
}
throw new Error(errorMessage);
}
// 2. 解析响应体(此处假设为 JSON)
return await response.json();
} catch (error) {
// 3. 捕获网络错误或上面抛出的业务错误
console.error('Fetch operation failed:', error);
// 可以根据需要重新抛出,或返回一个默认值
throw error;
}
}
7. 中止请求 (AbortController)
通过 AbortController 可以在请求未完成时取消它,避免浪费资源,常用于搜索框实时提示、切换页面时清理。
js
const controller = new AbortController();
const signal = controller.signal;
// 发起请求,传入 signal
fetch('https://api.example.com/slow-data', { signal })
.then(res => res.json())
.then(data => console.log('数据:', data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error(err);
}
});
// 5秒后强制取消
setTimeout(() => controller.abort(), 5000);
实用场景(防抖搜索):
js
let currentController = null;
async function search(query) {
// 取消上一次未完成的请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal
});
const data = await res.json();
renderResults(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('搜索出错', err);
}
}
}
// 绑定输入框,每次输入时 search(value)
8. 处理流式数据 (ReadableStream)
Fetch 的 response.body 是一个 ReadableStream,可以增量读取数据,特别适合处理大文件下载或 ChatGPT 类似的流式输出。
js
async function streamResponse(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
console.log('收到片段:', result);
}
console.log('接收完成:', result);
}
获取下载进度: 通过从 Reader 中累计已读取字节数,然后除以 Content-Length 头部计算。
js
const response = await fetch('https://example.com/large-file.mp4');
const contentLength = +response.headers.get('Content-Length');
const reader = response.body.getReader();
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedLength += value.length;
console.log(`进度: ${((receivedLength / contentLength) * 100).toFixed(2)}%`);
}
9. Request 对象的复用
有时需要多次发起配置相似的请求,可以创建一个 Request 对象,但它只能用于一次 fetch ,如需复制可调用 .clone()。
js
const req = new Request('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
// 第一次使用
fetch(req.clone()).then(/* ... */);
// 第二次使用原对象
fetch(req).then(/* ... */);
10. 跨域与凭证
默认情况下,fetch 不会发送 Cookie 等凭证信息。如需发送,必须设置 credentials: 'include',且服务器需要返回正确的 CORS 头(Access-Control-Allow-Credentials: true,且不能使用通配符 *)。
js
fetch('https://api.otherdomain.com/private', {
credentials: 'include' // 同源可用 'same-origin'
})
11. 超时处理(结合 Promise.race)
利用 Promise.race 让 fetch 请求与一个延时 reject 的 Promise 竞速,哪个先完成就采用哪个结果。同时结合 AbortController 确保超时后真正中止网络请求,避免资源浪费。
js
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
const fetchPromise = fetch(url, { ...options, signal });
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
controller.abort(); // 主动中止底层请求
reject(new Error(`请求超时 (${timeout}ms)`));
}, timeout);
});
// 两个 Promise 竞速
return Promise.race([fetchPromise, timeoutPromise]);
}
// 使用示例
fetchWithTimeout('https://api.example.com/slow', {}, 3000)
.then(res => res.json())
.then(data => console.log('数据:', data))
.catch(err => {
if (err.name === 'AbortError') {
console.error('请求被中止');
} else {
console.error('错误:', err.message);
}
});
说明:
fetchPromise正常发起请求。timeoutPromise在指定时间后 reject 并调用controller.abort()。Promise.race返回先完成的 Promise 结果:若请求在超时前完成则正常 resolve;若超时则 reject,fetch也会因signal被中止而抛出AbortError(可通过错误处理区分)。
12. 简单封装示范
将常用功能封装成一个更易用的工具函数:
js
async function http(url, options = {}) {
const defaults = {
headers: {
'Content-Type': 'application/json',
},
};
const config = {
...defaults,
...options,
headers: { ...defaults.headers, ...options.headers }
};
// 如果 body 是普通对象,转为 JSON 字符串
if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) {
config.body = JSON.stringify(config.body);
}
const response = await fetch(url, config);
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
if (!response.ok) {
throw { status: response.status, message: data.message || response.statusText, data };
}
return data;
}
// 使用
http('/api/users', { method: 'POST', body: { name: '王五' } })
.then(console.log)
.catch(err => console.error('错误:', err));
13. 关于是否需要手动配置 Content-Type
简单来说,是否需要手动设置 Content-Type 头,完全取决于你在请求体 (body) 中放入了什么类型的数据。 浏览器会根据数据对象类型,有一些自动行为。
🤖 自动设置的情形(不要手动干预)
当 body 是以下特定类型时,浏览器会自动生成并设置正确的 Content-Type ,你绝不应该手动设置,否则反而会导致错误。
body 数据类型 |
自动设置的 Content-Type 值 |
说明 |
|---|---|---|
FormData |
multipart/form-data; boundary=... |
这是文件上传的标准方式。boundary 由浏览器自动生成,用于分隔不同的表单字段。如果你手动设置 Content-Type,边界字符串会丢失,服务器将无法解析数据。 |
URLSearchParams |
application/x-www-form-urlencoded;charset=UTF-8 |
这是传统表单提交的编码方式。浏览器会帮你处理好编码。 |
Blob 或 File (当它们自带 type 属性时) |
使用 blob.type 的值,例如 image/png |
如果你创建 Blob 时指定了 { type: 'image/png' },浏览器会直接沿用这个值。虽然可以手动覆盖,但通常不需要。 |
代码示例(自动处理,无需设置):
js
// 1. FormData:自动生成 boundary
const formData = new FormData();
formData.append('username', 'Tom');
formData.append('avatar', fileInput.files[0]);
await fetch('/upload', { method: 'POST', body: formData });
// 请求头自动变成: multipart/form-data; boundary=----WebKitFormBoundary...
// 2. URLSearchParams:自动编码
const params = new URLSearchParams({ key: 'value', page: 1 });
await fetch('/search', { method: 'POST', body: params });
// 请求头自动变成: application/x-www-form-urlencoded;charset=UTF-8
✋ 必须手动设置的情形
当 body 是普通字符串 (Plain String)或其他浏览器无法推断类型的对象 时,浏览器就"不知道"它是什么内容了。此时默认不会添加 Content-Type 头,你需要显式告诉服务器数据的格式。
最典型、最常需要手动设置的情况是发送 JSON 数据 ,因为 JSON.stringify() 的结果就是一个字符串。
body 数据类型 |
需要手动设置的 Content-Type 值 |
说明 |
|---|---|---|
| JSON 字符串 | application/json |
必须设置,否则服务器通常无法正确解析请求体。 |
| 普通文本字符串 | text/plain |
如果你想让服务器明确知道这是纯文本,可以设置。 |
| XML 字符串 | text/xml 或 application/xml |
发送 XML 数据时需设置。 |
ArrayBuffer / TypedArray / DataView |
如 application/octet-stream 等 |
浏览器不会自动添加,需根据二进制数据的实际含义手动设置。 |
代码示例(必须手动设置):
js
// 发送 JSON 数据 ------ 这是最常见的手动设置场景
const data = { name: 'Alice', age: 30 };
await fetch('/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // 必须!
body: JSON.stringify(data)
});
⚠️ 特别提醒:关于 FormData 的常见误区
许多人误以为需要这样写:
js
// ❌ 错误做法:手动设置 Content-Type 但丢失了 boundary
headers: { 'Content-Type': 'multipart/form-data' }
服务器收到后会发现缺少边界分隔符,导致文件上传失败。请记住:只要用了 FormData,就完全不要碰 Content-Type 头。
总结对比表
| 特性 | fetch | XMLHttpRequest |
|---|---|---|
| 语法风格 | Promise,链式/async-await | 回调,稍显冗余 |
| 错误处理 | 仅网络错误 reject,需手动检查状态码 | onerror 事件,同样需判断 status |
| 请求/响应流 | 内置 ReadableStream 支持流式读取 |
可通过 responseType 设置,流式处理较复杂 |
| 中止请求 | AbortController |
xhr.abort() |
| 进度监听 | 无原生进度事件,需通过 Reader 推算 | 有 progress 事件 |
| Cookie 控制 | 默认不发送,需设 credentials |
默认发送同源 Cookie |
| 跨域 | 支持 CORS、no-cors 等模式 | 受同源策略限制,需设置 withCredentials |
| 使用便利性 | 更现代,代码简洁 | API 历史悠久,兼容性极好 |