如何使用 JavaScript Fetch API – wiki基地


深入浅出:JavaScript Fetch API 全面指南

在现代 Web 开发中,与服务器进行异步通信是核心功能之一。无论是获取数据、提交表单还是实现实时更新,浏览器都需要一种高效、灵活的方式来发送 HTTP 请求并处理响应。长期以来,XMLHttpRequest (XHR) 是这一领域的标准,但其复杂的事件驱动模型和冗长的语法常常让开发者感到头疼。

随着 Web 技术的演进,Fetch API 应运而生,提供了一种更强大、更灵活、更易于理解的方式来处理网络请求。Fetch API 基于 Promise,这使得处理异步操作变得更加直观和简洁,与现代 JavaScript 的异步编程模式(如 async/await)完美契合。

本文将带你深入了解 Fetch API,从最基础的用法到高级特性,帮助你掌握在各种场景下高效、可靠地使用 Fetch API。

1. 初识 Fetch API:基本用法与 Promise

Fetch API 的核心是一个全局的 fetch() 方法,它至少接受一个参数:要请求的资源的 URL。这个方法返回一个 Promise,该 Promise 在网络请求完成时解析(resolve),解析的值是一个 Response 对象。

让我们从一个简单的 GET 请求开始:

javascript
fetch('https://api.example.com/users')
.then(response => {
// response 是 Response 对象,表示服务器的响应
console.log('Response received:', response);
// 通常,我们需要将响应体解析成可用的数据格式(如 JSON)
// response.json() 也返回一个 Promise
return response.json();
})
.then(data => {
// data 是解析后的 JSON 数据
console.log('Data received:', data);
// 在这里处理获取到的数据,例如更新 DOM
})
.catch(error => {
// 捕获网络错误或解析错误
console.error('There was a problem with the fetch operation:', error);
});

解释:

  1. fetch('https://api.example.com/users'): 发起一个 GET 请求到指定的 URL。它立即返回一个 Promise。
  2. .then(response => { ... }): 当请求成功(或者说,当 Fetch 能够从服务器接收到响应头)时,第一个 .then() 回调会被执行。response 参数是一个 Response 对象。
    • 注意: fetch() Promise 在网络错误(如断网)或请求被阻止(如 CORS 问题)时才会被拒绝(reject)。即使服务器返回像 404 Not Found 或 500 Internal Server Error 这样的 HTTP 错误状态码,Fetch Promise 仍然会解析,只不过 Response 对象的 ok 属性会是 false。我们将在错误处理部分详细讨论这一点。
    • response 对象包含关于响应的信息,如状态码 (response.status)、状态文本 (response.statusText)、头部信息 (response.headers) 等。
    • response.json()response.text() 等方法用于异步读取响应体的内容并将其解析为特定的格式。这些方法也返回 Promise。我们必须再次使用 .then() 来处理解析后的数据。
  3. .then(data => { ... }): 第二个 .then() 回调处理由 response.json() 解析得到的实际数据。
  4. .catch(error => { ... }): 如果在 Fetch 请求过程中发生网络错误,或者在 .then() 链中的任何地方发生错误(包括 Promise 被拒绝,或在回调函数中抛出同步错误),.catch() 回调会被执行,并接收到错误对象。

这种 Promise 链式调用是处理异步操作的标准模式,它使得代码逻辑更加清晰。

2. 使用 async/await 简化 Fetch 代码

虽然 Promise 链式调用是有效的,但使用 async/await 语法可以进一步简化异步代码,使其看起来更像同步代码。这是处理 Fetch 请求的现代、推荐方式。

“`javascript
async function fetchUsers() {
try {
const response = await fetch(‘https://api.example.com/users’);

// 检查 HTTP 响应状态码
if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Data received (async/await):', data);
// 在这里处理获取到的数据
return data;

} catch (error) {
// 捕获网络错误或 HTTP 错误或解析错误
console.error(‘There was a problem with the fetch operation:’, error);
throw error; // 可以选择重新抛出错误
}
}

// 调用异步函数
fetchUsers();
“`

解释:

  1. async function fetchUsers() { ... }: 定义一个异步函数。async 关键字允许你在函数体内部使用 await
  2. const response = await fetch('...'): await 暂停函数的执行,直到 fetch() Promise 解析。解析后,Promise 的值(Response 对象)被赋给 response 变量。
  3. if (!response.ok) { ... }: 重要步骤! 在使用 async/await 时,你应该显式检查 response.ok (response.status >= 200 && response.status < 300) 来判断 HTTP 请求是否成功。如果状态码表示错误,我们手动抛出一个 Error
  4. const data = await response.json(): 再次使用 await 等待 response.json() Promise 解析,获取解析后的数据。
  5. try { ... } catch (error) { ... }: try...catch 块用于捕获异步函数内部的错误。这包括 fetch() 遇到的网络错误(Promise 被拒绝)、response.ok 检查失败时手动抛出的错误,或者 response.json() 解析失败时的错误。

async/await 使得 Fetch 代码的逻辑流程更加线性,更易于阅读和调试。在大多数现代应用中,强烈推荐使用 async/await 来处理 Fetch 请求。

3. Response 对象详解

fetch() 返回的 Response 对象包含了服务器响应的所有信息。以下是一些常用的属性和方法:

  • 属性:
    • Response.ok: 只读布尔值,表示响应的状态码是否在 200-299 范围内。
    • Response.status: 只读整数,表示 HTTP 状态码(例如 200, 404, 500)。
    • Response.statusText: 只读字符串,表示 HTTP 状态文本(例如 “OK”, “Not Found”)。
    • Response.headers: 只读 Headers 对象,包含响应的所有 HTTP 头部信息。
    • Response.url: 只读字符串,表示响应的最终 URL(考虑了重定向)。
    • Response.type: 只读字符串,表示响应的类型(例如 “basic”, “cors”, “opaque”)。
    • Response.redirected: 只读布尔值,表示响应是否是经过重定向得到的。
  • 方法(用于读取响应体,都返回 Promise):
    • Response.json(): 将响应体解析为 JSON 对象。
    • Response.text(): 将响应体解析为文本字符串。
    • Response.blob(): 将响应体解析为 Blob 对象(适用于二进制数据,如图片)。
    • Response.arrayBuffer(): 将响应体解析为 ArrayBuffer 对象(适用于更低级的二进制数据处理)。
    • Response.formData(): 将响应体解析为 FormData 对象(如果响应体是表单数据)。

示例:获取响应头部信息和状态码

“`javascript
async function checkStatusAndHeaders(url) {
try {
const response = await fetch(url);

console.log(`URL: ${response.url}`);
console.log(`Status: ${response.status} ${response.statusText}`);
console.log(`OK: ${response.ok}`);
console.log(`Redirected: ${response.redirected}`);

// 遍历响应头部
console.log('Response Headers:');
response.headers.forEach((value, name) => {
  console.log(`  ${name}: ${value}`);
});

// 检查特定的头部,例如 Content-Type
const contentType = response.headers.get('Content-Type');
console.log(`Content-Type: ${contentType}`);

// 根据 Content-Type 处理响应体
if (contentType && contentType.includes('application/json')) {
  const data = await response.json();
  console.log('Response Body (JSON):', data);
} else {
  const text = await response.text();
  console.log('Response Body (Text):', text.substring(0, 100) + '...'); // 打印部分文本
}

} catch (error) {
console.error(‘Error during fetch:’, error);
}
}

// 示例调用
checkStatusAndHeaders(‘https://rickandmortyapi.com/api/character/1’);
checkStatusAndHeaders(‘https://example.com’); // 可能会返回 HTML
“`

4. 发送不同类型的请求:POST, PUT, DELETE 等

默认情况下,fetch() 发送 GET 请求。要发送其他类型的请求,你需要向 fetch() 方法的第二个参数传递一个选项对象。

javascript
fetch(url, options)

选项对象是一个配置对象,常用的属性包括:

  • method: HTTP 请求方法,如 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'。默认为 'GET'
  • headers: 一个 Headers 对象或一个包含键值对的普通对象,用于设置请求头。
  • body: 请求体。对于 GET 或 HEAD 方法通常不使用。对于 POST, PUT 等方法,可以是一个 string, Blob, BufferSource, FormData, URLSearchParams, 或 ReadableStream 对象。
  • mode: 请求模式,如 'cors', 'no-cors', 'same-origin'。默认为 'cors'。影响跨域请求的处理。
  • credentials: 如何处理 cookie、认证头等凭证。可选值有 'omit', 'same-origin', 'include'。默认为 'omit'
  • cache: 如何处理请求的缓存。可选值有 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'。默认为 'default'
  • redirect: 如何处理重定向。可选值有 'follow', 'error', 'manual'。默认为 'follow'
  • referrer: 设置 Referer 头部。
  • integrity: 用于 Subresource Integrity 检查。
  • signal: 一个 AbortSignal 对象,用于取消请求(后面详细介绍)。

示例:发送 POST 请求并提交 JSON 数据

这是 Web API 中最常见的 POST 请求场景,通常用于创建资源或提交表单数据(作为 JSON)。

“`javascript
async function postData(url, data) {
try {
const response = await fetch(url, {
method: ‘POST’, // 设置请求方法为 POST
headers: {
‘Content-Type’: ‘application/json’ // 告诉服务器请求体是 JSON 格式
// 可以添加其他头部,例如认证 token: ‘Authorization’: ‘Bearer your_token’
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串作为请求体
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const responseData = await response.json();
console.log('POST successful:', responseData);
return responseData;

} catch (error) {
console.error(‘Error during POST:’, error);
throw error;
}
}

// 示例调用:向 API 发送一个新用户对象
const newUser = {
name: ‘John Doe’,
email: ‘[email protected]’,
age: 30
};
postData(‘https://api.example.com/users’, newUser);
“`

注意:

  • 当发送 POST、PUT 或 PATCH 请求并携带数据时,通常需要设置 Content-Type 头部,告诉服务器请求体的格式。常见的有 application/jsonapplication/x-www-form-urlencoded (用于传统的网页表单提交) 或 multipart/form-data (用于文件上传)。
  • body 的值必须是你想要发送的数据。如果发送 JSON,你需要使用 JSON.stringify() 将 JavaScript 对象转换为 JSON 字符串。

示例:发送 PUT 请求更新数据

PUT 请求通常用于完全替换现有资源。

``javascript
async function updateData(url, id, updatedData) {
try {
const response = await fetch(
${url}/${id}`, { // 通常在 URL 中包含资源 ID
method: ‘PUT’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(updatedData)
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

// PUT 请求的响应体可能包含更新后的资源,也可能只返回状态码
// 根据 API 的设计来决定是否需要解析响应体
if (response.status === 204) { // 204 No Content 是常见的成功状态码
  console.log(`Resource ${id} updated successfully (No Content)`);
  return null; // 或者根据需要返回其他值
} else {
  const responseData = await response.json();
  console.log(`Resource ${id} updated successfully:`, responseData);
  return responseData;
}

} catch (error) {
console.error(Error during PUT for resource ${id}:, error);
throw error;
}
}

// 示例调用:更新 ID 为 1 的用户数据
const updatedUser = {
name: ‘John Doe Updated’,
age: 31
};
updateData(‘https://api.example.com/users’, 1, updatedUser);
“`

示例:发送 DELETE 请求

DELETE 请求用于删除指定资源。通常没有请求体。

``javascript
async function deleteData(url, id) {
try {
const response = await fetch(
${url}/${id}`, {
method: ‘DELETE’ // 设置请求方法为 DELETE
// DELETE 请求通常不需要 body 和 Content-Type
// 但可能需要认证头部
// headers: { ‘Authorization’: ‘Bearer your_token’ }
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

// DELETE 成功可能返回 200 OK (带响应体), 202 Accepted, 或 204 No Content
if (response.status === 204) {
  console.log(`Resource ${id} deleted successfully (No Content)`);
} else {
  // 如果 API 返回 200 OK 并包含响应体
  try {
     const responseData = await response.json();
     console.log(`Resource ${id} deleted successfully:`, responseData);
  } catch (jsonError) {
     // 如果没有 JSON 响应体
     console.log(`Resource ${id} deleted successfully (Status: ${response.status})`);
  }
}

} catch (error) {
console.error(Error during DELETE for resource ${id}:, error);
throw error;
}
}

// 示例调用:删除 ID 为 1 的用户
deleteData(‘https://api.example.com/users’, 1);
“`

5. 处理请求头部 (Headers)

请求头部在 Fetch API 中由 Headers 对象表示。你可以通过在选项对象中提供一个普通对象或一个 Headers 实例来设置请求头部。

“`javascript
// 使用普通对象设置头部
fetch(url, {
method: ‘GET’,
headers: {
‘Content-Type’: ‘application/json’,
‘Authorization’: ‘Bearer your_token’,
‘Custom-Header’: ‘value’
}
});

// 使用 Headers 对象设置头部
const myHeaders = new Headers();
myHeaders.append(‘Content-Type’, ‘application/json’);
myHeaders.append(‘Authorization’, ‘Bearer your_token’);
myHeaders.set(‘Custom-Header’, ‘another value’); // set 会覆盖同名头部

fetch(url, {
method: ‘GET’,
headers: myHeaders
});
“`

Headers 对象的方法:

  • append(name, value): 添加一个头部。如果该头部已存在,则会添加一个新的同名头部(有些头部允许多个值)。
  • set(name, value): 设置或替换一个头部。如果该头部已存在,其值会被覆盖。
  • get(name): 获取指定头部的第一个值。
  • has(name): 检查是否存在指定的头部。
  • delete(name): 删除指定的头部。
  • forEach((value, name, headers) => { ... }): 遍历所有头部。

6. 发送不同类型的请求体数据

Fetch API 的 body 选项非常灵活,可以接受多种类型的数据:

  • 字符串: 最简单的情况,例如发送 JSON 字符串 (JSON.stringify(data)) 或纯文本。
  • FormData: 用于发送类似于 HTML 表单提交的数据,支持键值对和文件上传。
  • URLSearchParams: 用于构建 URL 查询字符串或作为 application/x-www-form-urlencoded 类型的请求体。
  • BlobFile: 用于上传二进制数据(如图片、文件)。
  • ArrayBufferTypedArray: 更低级的二进制数据。

示例:使用 FormData 发送数据

FormData 对于模拟传统的表单提交(enctype="multipart/form-data")或发送键值对(enctype="application/x-www-form-urlencoded")以及文件上传非常有用。Fetch API 会自动设置正确的 Content-Type (通常是 multipart/form-data) 和边界字符串。

“`javascript
async function submitFormWithFormData(url, formData) {
try {
const response = await fetch(url, {
method: ‘POST’,
body: formData // 直接传递 FormData 对象
// Fetch API 会自动设置 Content-Type: multipart/form-data
// headers: { ‘Content-Type’: ‘multipart/form-data’ } // 不要手动设置这个,Fetch 会处理
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();
console.log('Form submission successful:', result);
return result;

} catch (error) {
console.error(‘Error submitting form:’, error);
throw error;
}
}

// 创建 FormData 实例并添加数据
const myFormData = new FormData();
myFormData.append(‘username’, ‘testuser’);
myFormData.append(‘password’, ‘password123’);

// 假设有一个文件输入框
const fileInput = document.getElementById(‘avatarFile’);
if (fileInput && fileInput.files.length > 0) {
myFormData.append(‘avatar’, fileInput.files[0], fileInput.files[0].name); // 添加文件
}

// 调用函数提交数据
// submitFormWithFormData(‘https://api.example.com/upload’, myFormData); // 用于文件上传
// submitFormWithFormData(‘https://api.example.com/submit’, myFormData); // 用于普通表单数据
“`

示例:使用 URLSearchParams 发送 application/x-www-form-urlencoded

虽然 FormData 也可以用于这种场景,但 URLSearchParams 更适合构建查询字符串或这种特定的请求体格式。你需要手动设置 Content-Type 头部。

“`javascript
async function postUrlEncodedData(url, data) {
try {
const params = new URLSearchParams();
for (const key in data) {
if (data.hasOwnProperty(key)) {
params.append(key, data[key]);
}
}

    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: params.toString() // URLSearchParams 的 toString() 方法返回编码后的字符串
    });

    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json(); // 或 response.text()
    console.log('URL-encoded post successful:', result);
    return result;

} catch (error) {
    console.error('Error posting URL-encoded data:', error);
    throw error;
}

}

// 示例数据
const loginData = {
username: ‘user123’,
password: ‘securepassword’
};

// 调用函数
// postUrlEncodedData(‘https://api.example.com/login’, loginData);
“`

7. 处理跨域请求 (CORS)

Fetch API 遵循同源策略。默认情况下,它会进行 CORS 请求,这意味着它会发送 Origin 头部,并且浏览器会检查服务器响应中的 CORS 头部(如 Access-Control-Allow-Origin)来确定是否允许该跨域请求。

mode 选项控制请求模式:

  • 'cors' (默认): 标准的 CORS 请求。如果服务器没有返回正确的 CORS 头部,浏览器会阻止响应,Fetch Promise 会解析但 response.ok 可能不是 true(取决于具体错误),或者在某些情况下 Promise 可能会被拒绝(例如网络错误,或复杂的 CORS 预检请求失败)。
  • 'no-cors': 尽量减少对服务器 CORS 策略的影响。请求方法仅限于 GET, POST, HEAD。只允许少量简单的头部。响应的 type 会是 'opaque',你无法访问响应的绝大部分属性(如状态码、头部、响应体),这使得这种模式主要用于发送数据到其他域,而不需要读取响应。不推荐用于常规数据获取
  • 'same-origin': 只允许向同源地址发送请求。如果目标地址不同源,Fetch Promise 会被拒绝。
  • 'navigate': 用于导航请求。

credentials 选项控制是否发送 cookies、HTTP 认证头部等凭证:

  • 'omit' (默认): 不发送任何凭证。
  • 'same-origin': 只向同源地址发送凭证。
  • 'include': 总是发送凭证(即使是跨域请求)。如果使用 'include' 进行跨域请求,服务器必须返回 Access-Control-Allow-Credentials: true 头部,并且 Access-Control-Allow-Origin 不能是通配符 *,而必须是具体的源。

示例:跨域请求携带 cookie

“`javascript
async function fetchWithCredentials(url) {
try {
const response = await fetch(url, {
method: ‘GET’, // 或其他方法
mode: ‘cors’, // 确保是 CORS 模式
credentials: ‘include’ // 包含 cookie 和认证头部
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Data received with credentials:', data);
return data;

} catch (error) {
console.error(‘Error fetching with credentials:’, error);
throw error;
}
}

// 调用示例 (确保目标 API 支持 CORS 且允许 credentials: include)
// fetchWithCredentials(‘https://api.example.com/sensitive-data’);
“`

8. 取消请求:使用 AbortController

在单页应用中,用户可能会在请求完成前离开页面或执行新的操作,这时之前的网络请求可能不再需要。取消进行中的 Fetch 请求可以节省带宽和服务器资源,并防止不必要的副作用(例如在已卸载的组件中更新状态)。

Fetch API 提供了 AbortController 接口来实现请求取消。

“`javascript
// 在需要的地方创建 AbortController 实例
const controller = new AbortController();
const signal = controller.signal; // 获取 AbortSignal 对象

async function fetchDataWithCancellation(url, signal) {
try {
const response = await fetch(url, { signal }); // 将 signal 传递给 fetch 选项

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Data received:', data);
return data;

} catch (error) {
// 检查错误是否是由于 AbortError 引起的
if (error.name === ‘AbortError’) {
console.log(‘Fetch aborted’);
} else {
console.error(‘Error during fetch:’, error);
throw error;
}
}
}

// 发起请求
const fetchPromise = fetchDataWithCancellation(‘https://api.example.com/large-dataset’, signal);

// 稍后,如果需要取消请求
// 例如,在组件卸载时,或者用户点击取消按钮时
// 调用 AbortController 的 abort() 方法
// controller.abort();

// Promise 会被拒绝,并且 catch 块会捕获到一个 AbortError
“`

解释:

  1. 创建一个 AbortController 实例。
  2. controller.signal 获取一个 AbortSignal 对象。
  3. signal 对象作为 fetch() 选项对象的 signal 属性值。
  4. 当你想取消请求时,调用 controller.abort()
  5. abort() 被调用时,与该 signal 关联的任何 Fetch 请求都会被中止。 Fetch Promise 会被拒绝,拒绝的原因是一个名为 'AbortError'DOMException
  6. catch 块中,你可以通过检查 error.name === 'AbortError' 来区分是请求取消还是其他类型的错误。

AbortController 是处理长期运行请求(如流媒体、轮询、或用户快速切换页面)时非常有用的模式。

9. 错误处理的进阶

前面我们已经触及了错误处理,但有几个点需要更深入地理解:

  • 网络错误 vs. HTTP 错误:

    • 网络错误 (Network Errors): Fetch Promise 被拒绝 的情况。例如,用户断开网络、服务器无响应、CORS 预检请求失败等。这些错误会被 .catch() 捕获,或者在使用 async/await 时被 try...catch 捕获。
    • HTTP 错误 (HTTP Errors): 服务器返回非 2xx 状态码,如 404 (Not Found), 500 (Internal Server Error), 401 (Unauthorized) 等。Fetch Promise 仍然会解析,但 Response 对象的 ok 属性为 false。你需要手动检查 response.okresponse.status 并在需要时抛出错误。
  • 响应体解析错误: response.json()response.text() 等方法也会返回 Promise。如果响应体不是有效的 JSON (当调用 response.json() 时),这个 Promise 会被拒绝。这个错误也会被后续的 .catch()try...catch 捕获。

更健壮的错误处理函数

将 Fetch 请求和错误处理逻辑封装在一个函数中是很好的实践。

“`javascript
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);

// 1. 检查 HTTP 状态码
if (!response.ok) {
  // 尝试读取错误响应体(如果存在且是 JSON)
  let errorDetail = null;
  try {
      // 注意:读取响应体是异步的,也可能失败
      // clone() 方法用于创建一个响应的副本,因为响应体只能被读取一次
      // 我们尝试读取 JSON,如果失败,再尝试读取文本
      const errorResponseClone = response.clone();
      errorDetail = await response.json();
  } catch (jsonError) {
     try {
         const errorResponseClone = response.clone();
         errorDetail = await errorResponseClone.text();
     } catch (textError) {
         errorDetail = `Could not parse error response body: ${textError.message}`;
     }
  }

  const error = new Error(`HTTP error! Status: ${response.status}`);
  error.status = response.status; // 添加状态码到错误对象
  error.statusText = response.statusText; // 添加状态文本
  error.body = errorDetail; // 添加响应体内容
  console.error('HTTP Error Details:', error);
  throw error; // 抛出自定义错误,包含更多信息
}

// 2. 解析响应体(这里假设总是 JSON)
try {
   const data = await response.json();
   return data;
} catch (parseError) {
    console.error('Failed to parse response body as JSON:', parseError);
    const parseBody = await response.text(); // 尝试获取原始响应体文本
    const error = new Error(`Failed to parse JSON response: ${parseError.message}`);
    error.originalError = parseError;
    error.responseBody = parseBody; // 添加原始响应体
    throw error; // 抛出解析错误
}

} catch (networkError) {
// 3. 捕获网络错误或 AbortError
if (networkError.name === ‘AbortError’) {
console.warn(‘Fetch request aborted:’, url);
throw networkError; // 重新抛出 AbortError
} else {
console.error(‘Network or general fetch error:’, networkError);
throw new Error(Network or fetch failed for ${url}: ${networkError.message}); // 抛出网络错误
}
}
}

// 使用封装后的函数
async function loadData() {
try {
const users = await safeFetch(‘https://api.example.com/users’);
console.log(‘Loaded users:’, users);

// 尝试一个会失败的请求
// const nonExistentUser = await safeFetch('https://api.example.com/users/99999');

// 尝试一个返回非 JSON 的请求
// const htmlContent = await safeFetch('https://example.com'); // 这会触发解析错误

} catch (error) {
console.error(‘Error in loadData:’, error);
// 根据 error.status 或 error.name 进行更精细的处理
if (error.status === 404) {
console.log(“Resource not found.”);
} else if (error.name === ‘AbortError’) {
console.log(“Operation was cancelled by user.”);
} else {
console.log(“An unexpected error occurred:”, error.message);
}
}
}

loadData();
“`

在这个 safeFetch 函数中,我们:

  1. 首先等待 fetch Promise 解析,捕获网络级别的错误。
  2. 然后检查 response.ok。如果为 false,则表示 HTTP 错误。我们创建一个新的 Error 对象,附带状态码、状态文本,并尝试解析响应体以获取更详细的错误信息(许多 API 在错误响应中也返回 JSON 格式的错误详情)。
  3. 接着尝试解析响应体(例如,假设总是 JSON)。如果解析失败,也会捕获错误并抛出带有原始错误和响应体文本的新错误。
  4. 最后的 catch 块捕获前面所有步骤中抛出的错误,包括初始的网络错误、HTTP 错误以及解析错误。它还专门处理了 AbortError

这种封装使得业务逻辑代码更清晰,错误处理更集中和一致。

10. Fetch API 与 XMLHttpRequest 的比较

特性 Fetch API XMLHttpRequest (XHR)
API 设计 基于 Promise,链式调用或 async/await 基于事件,回调函数,状态机复杂
语法 简洁,易读 冗长,配置复杂
异步处理 自然地融入 Promise/async/await 异步流程 需要手动管理回调地狱或使用 Promise 包装
响应处理 Response 对象,多种读取响应体方法 (.json(), .text(), etc.),返回 Promise responseText, responseXML, responseType,需要手动解析
请求配置 第二个参数Options对象,属性清晰 通过各种 xhr.propertyNamexhr.methodName() 设置
头部操作 Headers 对象,具有标准方法 (append, set, get) setRequestHeader(), getAllResponseHeaders()
跨域控制 mode, credentials 选项,与标准 CORS 规范更贴合 通过属性和方法控制,相对分散
请求取消 AbortController,清晰的机制 需要在 XHR 实例上调用 abort(),与事件结合
进度事件 原生 Fetch 不直接支持上传/下载进度事件 内置支持 progress 事件
流 (Streaming) 支持通过 response.body 读取 ReadableStream 不支持原生流
Service Workers Fetch API 与 Service Workers 的拦截能力紧密集成 集成较弱

总结比较:

Fetch API 在 API 设计的现代性、易用性(尤其是在 Promise/async/await 环境下)、灵活性(处理各种数据类型、头部)和与新 Web 标准的集成(如 Service Workers, Streams, AbortController)方面都优于 XHR。尽管 XHR 在进度事件支持方面目前仍有优势,但在大多数 Web 开发场景下,Fetch API 是进行网络请求的首选。许多库(如 Axios)也选择在内部使用 XHR 或 Fetch API 作为其实现基础。

11. 最佳实践

  • 始终使用 async/await: 它让异步代码更易读、易写和维护。
  • 始终检查 response.ok: 不要依赖 Fetch Promise 的解析状态来判断 HTTP 请求是否成功。手动检查 response.okresponse.status
  • 恰当处理错误: 使用 try...catch 捕获网络错误和你在 response.ok 检查中手动抛出的 HTTP 错误。考虑解析响应体中的错误详情。
  • 使用 AbortController 处理不需要的请求: 在组件卸载、用户输入变化等场景下取消旧的请求。
  • 封装 Fetch 调用: 创建自己的函数(如上面示例中的 safeFetch)来处理通用的逻辑,例如设置默认头部、处理认证、规范化错误处理等。这能提高代码的复用性和可维护性。
  • 设置适当的头部: 特别是对于 POST/PUT 请求,正确设置 Content-Type。对于需要认证的请求,设置 Authorization 头部。
  • 注意 CORS: 理解 modecredentials 选项,以及服务器需要进行的相应配置。跨域问题是 Fetch 初学者常遇到的障碍。
  • 考虑加载状态: 在发起 Fetch 请求时,通常需要在 UI 上显示加载指示器,请求完成后移除它,无论成功或失败。
  • 处理用户反馈: 请求失败时,向用户提供有意义的错误消息。
  • 避免在循环中频繁发起 Fetch 请求: 如果需要获取大量资源,考虑后端是否提供批量获取的 API,或者优化请求策略。

12. 总结

Fetch API 是现代 JavaScript 中进行网络请求的强大且灵活的标准。它基于 Promise 的设计与 async/await 语法相结合,极大地改善了异步代码的可读性和可维护性,解决了 XMLHttpRequest 的许多痛点。

通过本文的学习,你应该已经掌握了:

  • 如何发起基本的 GET 请求并处理响应。
  • 如何使用 async/await 简化 Fetch 代码。
  • Response 对象的关键属性和方法。
  • 如何发送 POST、PUT、DELETE 等不同类型的请求。
  • 如何设置和管理请求头部。
  • 如何发送各种类型的请求体数据(JSON, FormData, URLSearchParams)。
  • 处理跨域请求时的 modecredentials 选项。
  • 使用 AbortController 取消进行中的请求。
  • 健壮的错误处理策略,区分网络错误、HTTP 错误和解析错误。
  • Fetch API 相对于 XMLHttpRequest 的优势。
  • 使用 Fetch API 进行开发时的最佳实践。

Fetch API 是现代前端开发中不可或缺的一部分。熟练掌握它将使你在构建需要与后端 API 交互的应用时事半功倍。不断实践和探索,你会发现它在各种复杂的网络通信场景中都能游刃有余。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部