JavaScript使用Fetch API发送HTTP请求的完整指南

作者:拾光时间:2026-11-03 22:52:32浏览:653

全面讲解现代 JavaScript 中使用原生 Fetch API 发送 HTTP 请求的完整指南。深入解析双阶段 Promise 机制、常见请求方式(GET/POST/PUT/DELETE)、多种响应格式解析、超时取消与错误处理最佳实践。

类型:技术实践指南 运行环境:浏览器 (ECMAScript 2015+) / Node.js 18+ 原生支持 规范标准:WHATWG Fetch Living Standard
Fetch API 是现代 JavaScript 中用于执行异步网络请求的原生接口,它基于 Promise 构建,全面取代了传统的 XMLHttpRequest。本文系统拆解 Fetch 的底层执行机制、常用 HTTP 方法(GET、POST、PUT、DELETE)的实战编码、不同数据载荷(JSON、FormData、URLSearchParams)的传输规范,以及生产环境中至关重要的状态码拦截、超时控制(AbortController)与跨域凭据处理。
JavaScript使用Fetch API发送HTTP请求的完整指南
图 0:Fetch API 客户端发起、安全沙箱验证与两阶段 Promise 响应处理全生命周期架构

一、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)并主动抛出异常。

JavaScript使用Fetch API发送HTTP请求的完整指南
图 1:Fetch 双阶段 Promise 决议流程、状态码分支与 catch 触发边界

二、发送常见 HTTP 请求与提交各类数据载荷

fetch() 方法接收两个参数:fetch(resource, [options])。resource 可以是请求目标的 URL 字符串或 Request 对象,options 是一个包含请求方法、请求头、主体内容等配置的 RequestInit 对象。

JavaScript使用Fetch API发送HTTP请求的完整指南
图 2:Fetch 现代架构优势与 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 请求是最常见的选择。需要特别注意两项配置:

  1. 将 headers 中的 Content-Type 设置为 application/json; charset=utf-8;
  2. 将 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();
}
JavaScript使用Fetch API发送HTTP请求的完整指南
图 3:Fetch 发送常见 HTTP 请求类型与数据处理流水线

三、响应处理与数据解析方法

服务端返回的 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。
JavaScript使用Fetch API发送HTTP请求的完整指南
图 4:Fetch 异常分支处理与健壮性保障决策全景图

五、快速排查与常见踩坑清单

  1. 排查一:接口返回 401/403/500,但未触发 catch 逻辑
    原因:Fetch 仅在发生物理网络中断或跨域被拦时进入 reject 分支。
    解法:在调用 await response.json() 前,务必编写 if (!response.ok) { throw new Error(...) }。
  2. 排查二:报错 TypeError: Failed to execute 'json' on 'Response': body stream already read
    原因:响应流已经被读取过一次,无法重复消费。
    解法:如果需要多次使用响应内容,请在第一次读取前执行 const clone = response.clone(),或将解析后的数据存入变量复用。
  3. 排查三:使用 FormData 上传文件时,后端接收不到字段或报错无法解析
    原因:手动指定了 headers: { 'Content-Type': 'multipart/form-data' },抹除了关键的 boundary 分隔标识。
    解法:完全移除 Content-Type 标头声明,由浏览器自动填充。
  4. 排查四:跨域请求无法自动携带 Session Cookie
    原因:Fetch 的 credentials 配置项默认为 'same-origin'。
    解法:在 options 中显式声明 credentials: 'include',并确保后端配置了对应的凭据响应头。
作者最新文章
相关文章
更多