全面讲解现代 JavaScript 中使用原生 Fetch API 发送 HTTP 请求的完整指南。深入解析双阶段 Promise 机制、常见请求方式(GET/POST/PUT/DELETE)、多种响应格式解析、超时取消与错误处理最佳实践。
JavaScript使用Fetch API发送HTTP请求的完整指南
时间:2026-11-03
浏览:649
文章目录
XMLHttpRequest。本文系统拆解 Fetch 的底层执行机制、常用 HTTP 方法(GET、POST、PUT、DELETE)的实战编码、不同数据载荷(JSON、FormData、URLSearchParams)的传输规范,以及生产环境中至关重要的状态码拦截、超时控制(AbortController)与跨域凭据处理。
一、Fetch API 的核心机制与基本语法
在了解具体请求发送方式之前,必须先掌握 Fetch 最具特色的两项核心设计:双阶段 Promise 决议与非网络错误不拒绝(No Rejection on HTTP Errors)。
1. 双阶段 Promise 工作原理
传统的 Ajax 请求往往在数据全部到达后一次性触发回调,而 Fetch 将网络交互清晰拆分为两个独立阶段:
- 第一阶段(握手与响应头就绪):调用
fetch(url)返回一个 Promise。只要目标服务器返回了 HTTP 状态码和响应报头(Headers),此 Promise 便立即进入resolved状态,返回一个代表响应元数据的Response对象。此时服务器的实体正文(Body)可能仍在网络流中传输,尚未完全下载到本地。 - 第二阶段(数据流读取与反序列化):通过调用
response.json()、response.text()或response.blob()等方法,触发第二个 Promise。该步骤会锁定并读取底层的ReadableStream数据流,在完整接收并转换完成后才最终 resolve 出具体的 JavaScript 数据。
2. 为什么 404 或 500 不会触发 catch 块?
这是初学者最容易踩坑的地方:当后端接口返回 404 Not Found 或 500 Internal Server Error 时,Fetch 返回的 Promise 依然会顺利 resolve,而不会触发 .catch()。
在 Fetch 的规范定义中,只要 HTTP 事务本身正常完成(DNS 解析成功、TCP 握手完成、服务端给出了 HTTP 应答),网络通信就是成功的。只有在发生以下严重通信中断或安全阻断时,Promise 才会 reject:
- 客户端断网或 Wi-Fi 信号丢失;
- 目标域名不存在或 DNS 解析失败;
- 浏览器的同源策略(CORS)阻断了请求;
- 使用
AbortController主动取消或超时熔断。
因此,在业务代码中,必须显式检查 response.ok(当 HTTP 状态码在 200 到 299 之间时为 true)并主动抛出异常。
二、发送常见 HTTP 请求与提交各类数据载荷
fetch() 方法接收两个参数:fetch(resource, [options])。resource 可以是请求目标的 URL 字符串或 Request 对象,options 是一个包含请求方法、请求头、主体内容等配置的 RequestInit 对象。
1. 发送 GET 请求并附加查询参数
GET 请求不能包含 body 属性。向服务端传递过滤条件、分页或搜索关键字时,参数必须附加在 URL 的查询字符串(Query String)中。推荐使用原生的 URLSearchParams 来组装参数,避免手动字符串拼接导致的特殊字符编码错误。
/**
* 示例:发送带查询参数的 GET 请求
* @param {string} endpoint 接口路径
* @param {Record<string, any>} params 查询参数键值对
*/
async function fetchArticles(endpoint, params) {
// 1. 利用 URLSearchParams 自动对中文、空格、特殊字符进行安全编码
const queryString = new URLSearchParams(params).toString();
const requestUrl = `${endpoint}?${queryString}`;
try {
const response = await fetch(requestUrl, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
// 2. 检查 HTTP 状态码是否处于 200-299 区间
if (!response.ok) {
throw new Error(`请求失败,状态码: ${response.status} (${response.statusText})`);
}
// 3. 读取并解析 JSON 响应体
const result = await response.json();
return result;
} catch (error) {
console.error('网络或解析异常:', error.message);
throw error;
}
}
// 执行调用示例
fetchArticles('https://api.example.com/v1/posts', { page: 1, limit: 10, tag: 'javascript' })
.then(data => console.log('获取数据成功:', data));
2. 发送 POST 请求并提交 JSON 数据
提交结构化数据时,POST 请求是最常见的选择。需要特别注意两项配置:
- 将
headers中的Content-Type设置为application/json; charset=utf-8; - 将 JavaScript 对象通过
JSON.stringify()转换为 JSON 字符串后赋值给body。如果直接传递普通 JS 对象,会被隐式转换为"[object Object]"导致后端解析失败。
/**
* 示例:发送 POST 请求提交 JSON 数据
*/
async function createPost(postData) {
const response = await fetch('https://api.example.com/v1/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
body: JSON.stringify(postData)
});
if (!response.ok) {
// 尝试提取服务端返回的错误信息 JSON
const errorBody = await response.json().catch(() => ({}));
throw new Error(errorBody.message || `创建资源失败: ${response.status}`);
}
return await response.json();
}
3. 提交表单与上传文件(FormData)
当需要向后端上传文件(图片、视频、文档)或提交类似传统 HTML 表单的数据时,应当使用 FormData 对象。
FormData 作为 body 时,切勿手动设置 headers: { 'Content-Type': 'multipart/form-data' }!手动设置会导致请求头缺失浏览器生成的 boundary 分界符,后端无法正确解析切片。浏览器会在检测到 FormData 实例时自动补全正确的 Content-Type 和 boundary 字段。
/**
* 示例:使用 FormData 上传文件与附加元数据
*/
async function uploadUserProfile(avatarFile, username) {
const formData = new FormData();
formData.append('username', username);
formData.append('avatar', avatarFile, avatarFile.name);
// 注意:不要在 headers 中写 'Content-Type': 'multipart/form-data'
const response = await fetch('https://api.example.com/v1/users/profile', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`上传失败,状态码: ${response.status}`);
}
return await response.json();
}
4. 发送 PUT、PATCH 与 DELETE 请求更新和删除资源
RESTful 架构中,资源的更新通常使用 PUT(全量覆盖)或 PATCH(增量更新),资源的销毁使用 DELETE。针对无返回内容的删除请求(常见 HTTP 状态码 204 No Content),调用 response.json() 会引发语法错误,需要提前判断。
/**
* 示例:删除指定 ID 的资源
*/
async function deleteResource(resourceId) {
const response = await fetch(`https://api.example.com/v1/items/${resourceId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`删除失败: ${response.status}`);
}
// 针对 204 No Content,无需尝试读取 JSON 响应体
if (response.status === 204) {
return { success: true, message: '资源删除成功' };
}
return await response.json();
}
三、响应处理与数据解析方法
服务端返回的 Response 实例提供了多种数据转换方法,支持处理不同 MIME 类型的实体正文。
| 解析方法 | 返回数据类型 | 典型适用场景 | 底层机制 |
|---|---|---|---|
response.json() |
Promise<any> |
REST API 接口数据交互 | 将 UTF-8 字节流读取完毕后执行 JSON.parse |
response.text() |
Promise<string> |
纯文本、HTML 片段、XML 字符串、CSV | 按编码解码为标准 JavaScript 字符串 |
response.blob() |
Promise<Blob> |
图像展示、PDF 预览、文件下载 | 封装为不可变的二进制原始数据对象 |
response.arrayBuffer() |
Promise<ArrayBuffer> |
底层二进制音频合成、WebGL 纹理、WebAssembly | 提供连续固定长度的原始二进制缓冲区 |
响应体只能读取一次(Body Used 规则)
Response 对象实现了 Body 混入类。因为响应体本质是一个只读的单向输入流(ReadableStream),一旦调用了 json() 或 text(),流就会被锁定并排干(response.bodyUsed 会置为 true)。
如果尝试对同一个响应对象再次调用解析方法:
const response = await fetch('/api/data');
const jsonData = await response.json(); // 第一次消费流:成功
const textData = await response.text(); // 第二次消费流:抛出 TypeError: Failed to execute 'text' on 'Response': body stream already read
解决方案:如果确实需要在不同模块中重复读取同一份响应(例如:一次用于业务解析,一次用于全局审计日志记录),必须在首次消费前使用 response.clone() 创建一份副本:
const response = await fetch('/api/data');
const clonedResponse = response.clone(); // 复制流分支
const businessData = await response.json();
const rawLogText = await clonedResponse.text();
四、生产环境必备进阶技能:错误捕获、超时控制与取消请求
1. 使用 AbortController 实现超时与主动取消
早期的 Fetch API 本身没有内置 timeout 配置项。如果后端接口发生死锁或网络链路挂起,请求可能会长时间悬挂。现代浏览器提供了 AbortController 接口来精确控制请求生命周期。
在最新标准中,可以使用原生的 AbortSignal.timeout(ms) 快速创建定时熔断信号;为了兼容更广泛的环境,可以使用标准的 setTimeout 手动触发 abort():
/**
* 具备精确超时控制的 fetch 封装函数
* @param {string} url 请求地址
* @param {RequestInit} options 配置项
* @param {number} timeout 超时时间(毫秒),默认 8000ms
*/
async function fetchWithTimeout(url, options = {}, timeout = 8000) {
// 1. 创建中止控制器实例
const controller = new AbortController();
const { signal } = controller;
// 2. 如果外部也传入了 signal,需要监听外部的中止事件
if (options.signal) {
options.signal.addEventListener('abort', () => controller.abort());
}
// 3. 启动定时器:超出指定时间未完成则调用 abort()
const timerId = setTimeout(() => {
controller.abort(new DOMException('请求超出指定时长,已被客户端终止', 'TimeoutError'));
}, timeout);
try {
const response = await fetch(url, {
...options,
signal
});
// 4. 请求顺利返回,清除定时器
clearTimeout(timerId);
if (!response.ok) {
throw new Error(`HTTP 异常: ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timerId);
// 5. 识别是否由超时或取消引发
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
console.warn('请求已超时或被用户主动取消');
} else {
console.error('网络或业务异常:', error.message);
}
throw error;
}
}
2. 跨域资源共享(CORS)与 Cookie 携带策略
当网页向不同源(域名、协议或端口不一致)的服务端发送 Fetch 请求时,浏览器会自动执行 CORS 校验:
- 对于包含自定义标头或复杂请求体(如
application/json)的请求,浏览器会在正式请求前自动触发OPTIONS预检请求(Preflight Request)。后端必须在预检应答中正确声明Access-Control-Allow-Origin与Access-Control-Allow-Headers。 - 凭据传递规则(credentials):默认情况下,Fetch 在跨域请求中不会自动携带目标域的 Cookie 或 HTTP 认证头。如果需要携带登录态 Cookie,必须显式配置
credentials: 'include';此时服务端的Access-Control-Allow-Origin绝对不能设置为通配符*,且必须声明Access-Control-Allow-Credentials: true。
五、快速排查与常见踩坑清单
-
排查一:接口返回 401/403/500,但未触发 catch 逻辑
原因:Fetch 仅在发生物理网络中断或跨域被拦时进入 reject 分支。
解法:在调用await response.json()前,务必编写if (!response.ok) { throw new Error(...) }。 -
排查二:报错
TypeError: Failed to execute 'json' on 'Response': body stream already read
原因:响应流已经被读取过一次,无法重复消费。
解法:如果需要多次使用响应内容,请在第一次读取前执行const clone = response.clone(),或将解析后的数据存入变量复用。 -
排查三:使用 FormData 上传文件时,后端接收不到字段或报错无法解析
原因:手动指定了headers: { 'Content-Type': 'multipart/form-data' },抹除了关键的boundary分隔标识。
解法:完全移除Content-Type标头声明,由浏览器自动填充。 -
排查四:跨域请求无法自动携带 Session Cookie
原因:Fetch 的credentials配置项默认为'same-origin'。
解法:在 options 中显式声明credentials: 'include',并确保后端配置了对应的凭据响应头。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。