Fetch API 的基本用法

65 阅读5分钟

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: 请求体,可以是字符串、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)
// 获取图片并显示
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 值说明
FormDatamultipart/form-data; boundary=...这是文件上传的标准方式。boundary 由浏览器自动生成,用于分隔不同的表单字段。如果你手动设置 Content-Type,边界字符串会丢失,服务器将无法解析数据。
URLSearchParamsapplication/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 能轻松嵌套在 ifforwhile 中,而链式则需要额外拆分或使用 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 的可维护性标准,也让团队协作更容易。如果某个请求真的非常短小且逻辑独立,用链式也无伤大雅,保持整个项目风格统一即可。


总结对比表

特性fetchXMLHttpRequest
语法风格Promise,链式/async-await回调,稍显冗余
错误处理仅网络错误 reject,需手动检查状态码onerror 事件,同样需判断 status
请求/响应流内置 ReadableStream 支持流式读取可通过 responseType 设置,流式处理较复杂
中止请求AbortControllerxhr.abort()
进度监听无原生进度事件,需通过 Reader 推算有 progress 事件
Cookie 控制默认不发送,需设 credentials默认发送同源 Cookie
跨域支持 CORS、no-cors 等模式受同源策略限制,需设置 withCredentials
使用便利性更现代,代码简洁API 历史悠久,兼容性极好