深入浅出: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);
});
解释:
fetch('https://api.example.com/users')
: 发起一个 GET 请求到指定的 URL。它立即返回一个 Promise。.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()
来处理解析后的数据。
- 注意:
.then(data => { ... })
: 第二个.then()
回调处理由response.json()
解析得到的实际数据。.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();
“`
解释:
async function fetchUsers() { ... }
: 定义一个异步函数。async
关键字允许你在函数体内部使用await
。const response = await fetch('...')
:await
暂停函数的执行,直到fetch()
Promise 解析。解析后,Promise 的值(Response
对象)被赋给response
变量。if (!response.ok) { ... }
: 重要步骤! 在使用async/await
时,你应该显式检查response.ok
(response.status >= 200 && response.status < 300
) 来判断 HTTP 请求是否成功。如果状态码表示错误,我们手动抛出一个Error
。const data = await response.json()
: 再次使用await
等待response.json()
Promise 解析,获取解析后的数据。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/json
、application/x-www-form-urlencoded
(用于传统的网页表单提交) 或multipart/form-data
(用于文件上传)。 body
的值必须是你想要发送的数据。如果发送 JSON,你需要使用JSON.stringify()
将 JavaScript 对象转换为 JSON 字符串。
示例:发送 PUT 请求更新数据
PUT 请求通常用于完全替换现有资源。
``javascript
${url}/${id}`, { // 通常在 URL 中包含资源 ID
async function updateData(url, id, updatedData) {
try {
const response = await fetch(
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
${url}/${id}`, {
async function deleteData(url, id) {
try {
const response = await fetch(
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
类型的请求体。Blob
或File
: 用于上传二进制数据(如图片、文件)。ArrayBuffer
或TypedArray
: 更低级的二进制数据。
示例:使用 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
“`
解释:
- 创建一个
AbortController
实例。 - 从
controller.signal
获取一个AbortSignal
对象。 - 将
signal
对象作为fetch()
选项对象的signal
属性值。 - 当你想取消请求时,调用
controller.abort()
。 - 当
abort()
被调用时,与该signal
关联的任何 Fetch 请求都会被中止。 Fetch Promise 会被拒绝,拒绝的原因是一个名为'AbortError'
的DOMException
。 - 在
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.ok
或response.status
并在需要时抛出错误。
- 网络错误 (Network Errors): Fetch Promise 被拒绝 的情况。例如,用户断开网络、服务器无响应、CORS 预检请求失败等。这些错误会被
-
响应体解析错误:
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
函数中,我们:
- 首先等待
fetch
Promise 解析,捕获网络级别的错误。 - 然后检查
response.ok
。如果为false
,则表示 HTTP 错误。我们创建一个新的Error
对象,附带状态码、状态文本,并尝试解析响应体以获取更详细的错误信息(许多 API 在错误响应中也返回 JSON 格式的错误详情)。 - 接着尝试解析响应体(例如,假设总是 JSON)。如果解析失败,也会捕获错误并抛出带有原始错误和响应体文本的新错误。
- 最后的
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.propertyName 和 xhr.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.ok
或response.status
。 - 恰当处理错误: 使用
try...catch
捕获网络错误和你在response.ok
检查中手动抛出的 HTTP 错误。考虑解析响应体中的错误详情。 - 使用
AbortController
处理不需要的请求: 在组件卸载、用户输入变化等场景下取消旧的请求。 - 封装 Fetch 调用: 创建自己的函数(如上面示例中的
safeFetch
)来处理通用的逻辑,例如设置默认头部、处理认证、规范化错误处理等。这能提高代码的复用性和可维护性。 - 设置适当的头部: 特别是对于 POST/PUT 请求,正确设置
Content-Type
。对于需要认证的请求,设置Authorization
头部。 - 注意 CORS: 理解
mode
和credentials
选项,以及服务器需要进行的相应配置。跨域问题是 Fetch 初学者常遇到的障碍。 - 考虑加载状态: 在发起 Fetch 请求时,通常需要在 UI 上显示加载指示器,请求完成后移除它,无论成功或失败。
- 处理用户反馈: 请求失败时,向用户提供有意义的错误消息。
- 避免在循环中频繁发起 Fetch 请求: 如果需要获取大量资源,考虑后端是否提供批量获取的 API,或者优化请求策略。
12. 总结
Fetch API 是现代 JavaScript 中进行网络请求的强大且灵活的标准。它基于 Promise 的设计与 async/await
语法相结合,极大地改善了异步代码的可读性和可维护性,解决了 XMLHttpRequest
的许多痛点。
通过本文的学习,你应该已经掌握了:
- 如何发起基本的 GET 请求并处理响应。
- 如何使用
async/await
简化 Fetch 代码。 Response
对象的关键属性和方法。- 如何发送 POST、PUT、DELETE 等不同类型的请求。
- 如何设置和管理请求头部。
- 如何发送各种类型的请求体数据(JSON, FormData, URLSearchParams)。
- 处理跨域请求时的
mode
和credentials
选项。 - 使用
AbortController
取消进行中的请求。 - 健壮的错误处理策略,区分网络错误、HTTP 错误和解析错误。
- Fetch API 相对于
XMLHttpRequest
的优势。 - 使用 Fetch API 进行开发时的最佳实践。
Fetch API 是现代前端开发中不可或缺的一部分。熟练掌握它将使你在构建需要与后端 API 交互的应用时事半功倍。不断实践和探索,你会发现它在各种复杂的网络通信场景中都能游刃有余。