Fetch API 是现代 JavaScript 中进行网络请求的核心方法。它提供了一个基于 Promise 的接口,用于替代老旧的 XMLHttpRequest,让开发者能以更简洁、更现代的方式处理 HTTP 请求与响应。
Fetch API 的核心是请求 (Request) 和响应 (Response) 这两个对象。使用时,你创建一个请求,fetch() 方法会返回一个 Promise,这个 Promise 最终会兑现为包含服务器返回数的 Response 对象。
下面从基础到进阶,详细拆解它的用法。
1. 基础语法
// 链式
fetch(resource, options)
.then(response => response.json())
.then(data => { ... })
.catch(error => { ... });
// async-await
const fn = async () => {
try {
const response = await fetch(resource, options);
if(!response.ok) throw new Error(...)
return 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) |
// 获取图片并显示
fetch('https://example.com/photo.jpg')
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
});
4. 发起简单 GET 请求
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 数据
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)
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(类传统表单)
const params = new URLSearchParams({
key1: 'value1',
key2: 'value2'
});
fetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
6. 错误处理完整模式
// 示例一
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 可以在请求未完成时取消它,避免浪费资源,常用于搜索框实时提示、切换页面时清理。
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);
实用场景(防抖搜索):
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 类似的流式输出。
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 头部计算。
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()。
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,且不能使用通配符 *)。
fetch('https://api.otherdomain.com/private', {
credentials: 'include' // 同源可用 'same-origin'
})
11. 超时处理(结合 Promise.race)
利用 Promise.race 让 fetch 请求与一个延时 reject 的 Promise 竞速,哪个先完成就采用哪个结果。同时结合 AbortController 确保超时后真正中止网络请求,避免资源浪费。
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. 简单封装示范
将常用功能封装成一个更易用的工具函数:
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' },浏览器会直接沿用这个值。虽然可以手动覆盖,但通常不需要。 |
代码示例(自动处理,无需设置):
// 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 等 | 浏览器不会自动添加,需根据二进制数据的实际含义手动设置。 |
代码示例(必须手动设置):
// 发送 JSON 数据 —— 这是最常见的手动设置场景
const data = { name: 'Alice', age: 30 };
await fetch('/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // 必须!
body: JSON.stringify(data)
});
⚠️ 特别提醒:关于 FormData 的常见误区
许多人误以为需要这样写:
// ❌ 错误做法:手动设置 Content-Type 但丢失了 boundary
headers: { 'Content-Type': 'multipart/form-data' }
服务器收到后会发现缺少边界分隔符,导致文件上传失败。请记住:只要用了 FormData,就完全不要碰 Content-Type 头。
14. 关于是否需要加 await
// ✅ 正确:使用 await,data 为解析后的对象
const data = await response.json();
console.log(data); // { name: '张三', age: 30 }
// ❌ 错误:缺少 await,data 是一个 Promise
const data = response.json();
console.log(data); // Promise { <pending> }
response.json()会读取响应流,并将文本解析为 JSON,这个过程是异步的。- 如果直接赋值:
data = response.json(),data得到的是Promise<object>。 - 只有通过
await或.then()才能取出解析后的结果。
在 async 函数中如何选择?
只要是 async 函数内部,且你想直接得到解析值,就必须 await。 如果你不 await,可以将 Promise 返回给调用者,由外层再 await:
async function getData() {
const response = await fetch('/api');
return response.json(); // 这里不需要 await,因为返回值会被外层函数的调用者 await
}
// 使用
const data = await getData(); // 这里 await 实际等待的是 response.json() 的 Promise
简单原则:你想把解析后的数据赋值给一个变量在当前作用域使用,就要 await;如果你想直接返回这个 Promise 让别人处理,可以不 await。
15. 语法风格
在现代 JavaScript 开发中,更推荐 async/await 风格,它通常比纯 .then() 链式调用更清晰、更易维护。不过两种方式各有适用场景。
1. 为什么首选 async/await?
(1)可读性像同步代码
异步逻辑用线性方式书写,避免了层层嵌套或长链的视觉割裂。
// async/await:一目了然
async function getUser() {
const res = await fetch('/api/user');
const data = await res.json();
console.log(data);
}
// 链式:逻辑被拆散到多个回调里
function getUser() {
fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data));
}
(2)错误处理更自然
可以用熟悉的 try...catch 统一捕获同步和异步错误,避免在链式末尾单独处理。
// async/await:集中错误处理
async function load() {
try {
const res = await fetch('/api');
if (!res.ok) throw new Error('服务端错误');
return await res.json();
} catch (err) {
console.error('请求失败:', err);
}
}
// 链式:错误处理散落在 .catch() 中
function load() {
fetch('/api')
.then(res => {
if (!res.ok) throw new Error('服务端错误');
return res.json();
})
.then(data => { /* ... */ })
.catch(err => console.error('请求失败:', err));
}
(3)调试友好
在 async/await 中,可以在 await 行打断点,像调试同步代码一样单步执行。链式调用的每一步都是一个独立回调,断点追踪更繁琐。
(4)条件逻辑和循环更简洁
async/await 能轻松嵌套在 if、for、while 中,而链式则需要额外拆分或使用 Promise 组合。
// async/await:循环请求
for (const id of [1, 2, 3]) {
const data = await fetch(`/item/${id}`).then(r => r.json());
console.log(data);
}
// 链式:需要 reduce 或递归
[1, 2, 3].reduce((promise, id) => {
return promise.then(() =>
fetch(`/item/${id}`).then(r => r.json()).then(console.log)
);
}, Promise.resolve());
2. 链式调用的适用场景
async/await 虽好,但 .then() 在某些情况下仍然非常合适:
- 简单无错误处理的“直线”操作:一行或两行即可完成,没必要用函数封装。
fetch('/data').then(res => res.json()).then(console.log); - 函数组合/管道:当需要将多个独立处理步骤串联,链式写法更显式。
- 不需要暂停执行上下文的场景:在模块顶层(非 async 函数内),可以直接返回 Promise 链。
总结建议
| 风格 | 推荐场景 | 不推荐场景 |
|---|---|---|
async/await | 复杂逻辑、多步异步操作、需要错误处理、条件/循环 | 极其简单的单行操作(有点杀鸡用牛刀) |
.then() 链 | 简单直连、函数式组合、模块顶层返回 | 多层嵌套、需要 try...catch 的错误处理 |
默认选择 async/await,能让你的代码更符合现代 JavaScript 的可维护性标准,也让团队协作更容易。如果某个请求真的非常短小且逻辑独立,用链式也无伤大雅,保持整个项目风格统一即可。
总结对比表
| 特性 | fetch | XMLHttpRequest |
|---|---|---|
| 语法风格 | Promise,链式/async-await | 回调,稍显冗余 |
| 错误处理 | 仅网络错误 reject,需手动检查状态码 | onerror 事件,同样需判断 status |
| 请求/响应流 | 内置 ReadableStream 支持流式读取 | 可通过 responseType 设置,流式处理较复杂 |
| 中止请求 | AbortController | xhr.abort() |
| 进度监听 | 无原生进度事件,需通过 Reader 推算 | 有 progress 事件 |
| Cookie 控制 | 默认不发送,需设 credentials | 默认发送同源 Cookie |
| 跨域 | 支持 CORS、no-cors 等模式 | 受同源策略限制,需设置 withCredentials |
| 使用便利性 | 更现代,代码简洁 | API 历史悠久,兼容性极好 |