Fetch API 的基本用法

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 : 请求体,可以是字符串、FormDataBlobURLSearchParams 等。
  • 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。即使服务器返回 404500 这样的错误状态码,fetch() 也仍然会 resolve。因此,必须手动检查 response.ok(当 HTTP 状态码在 200-299 范围内时为 trueresponse.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 这是传统表单提交的编码方式。浏览器会帮你处理好编码。
BlobFile (当它们自带 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/xmlapplication/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 历史悠久,兼容性极好
相关推荐
是上好佳佳佳呀2 小时前
【前端(十三)】JavaScript 数组与字符串笔记
前端·javascript·笔记
yanchGod3 小时前
CSS page-break-before 属性详解:控制打印分页的艺术
前端·javascript·css·html·css3·html5
卜凡.3 小时前
Vue是对HTML、CSS、JS的标准化、组件化和响应式的上层抽象与增强
javascript·vue.js·html
冰暮流星3 小时前
javascript之默认事件
开发语言·javascript·ecmascript
前端之虎陈随易4 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
大数据·linux·javascript·人工智能·算法·microsoft·typescript
kyriewen4 小时前
你等的Babel编译,够喝三杯咖啡了——用Rust重写的SWC,只需眨个眼
前端·javascript·rust
张元清4 小时前
SSR 状态管理陷阱:defineStore vs defineContextStore
前端·javascript·面试
openKaka_5 小时前
为什么 React 18 之后使用 createRoot,而不是 ReactDOM.render
前端·javascript·react.js