深入理解 JavaScript Fetch API:现代网络请求的利器
在现代 Web 开发中,与服务器进行数据交互是核心功能之一。无论是获取数据、提交表单,还是上传文件,客户端都需要向服务器发起 HTTP 请求。长期以来,JavaScript 中进行异步 HTTP 请求的标准方法是 XMLHttpRequest (XHR) 对象。然而,随着 Web 技术的不断发展和对更简洁、更强大异步编程模型的需求,Fetch API 应运而生,并迅速成为进行网络请求的主流方式。
Fetch API 提供了一种更现代、更灵活的方式来发起网络请求。它基于 Promise,这使得处理异步响应变得更加容易,并提供了更强大的功能,例如流式处理响应、以及对请求和响应的更多底层控制。本文将深入探讨 JavaScript Fetch API,从基础用法到高级特性,帮助你全面掌握这一重要的 Web API。
一、告别 XHR:Fetch API 的诞生背景与优势
在 Fetch API 出现之前,开发者主要依赖 XMLHttpRequest (XHR) 进行 AJAX 请求。虽然 XHR 功能强大且兼容性好,但它的 API 设计存在一些痛点:
- 基于事件回调: XHR 的异步操作依赖大量的事件监听(如
onreadystatechange,onload,onerror等),这使得代码容易陷入“回调地狱”,尤其是当需要处理多个相互依赖的请求时。 - API 冗余和不够直观: 设置请求头、请求体、处理不同类型的响应数据(JSON、文本、 Blob 等)需要调用多个不同的方法和属性,不够简洁。
- 对 Promise 支持不足: 虽然可以通过包装 XHR 来实现 Promise 化,但这并非原生支持,增加了额外的复杂性。
- 流式处理能力有限: 对于处理大型响应数据,XHR 不易实现流式处理。
Fetch API 的设计目标正是为了解决这些问题,提供一个更简洁、更强大、基于 Promise 的网络请求接口。
Fetch API 的核心优势包括:
- 基于 Promise: Fetch 返回的是一个 Promise,这使得异步操作的链式调用和错误处理变得非常优雅,与
async/await语法结合使用更是如虎添翼,代码可读性大大提高。 - 更清晰的 API: Fetch 的设计更符合 HTTP 协议的逻辑,通过
Request和Response对象来抽象请求和响应,属性和方法更直观。 - 分离头部和主体: Fetch 在接收响应时,首先解析头部信息,然后才开始读取主体。Promise 在接收到头部后就会解决 (resolve),后续再异步读取主体内容(如 JSON、文本等),这与 XHR 的
onreadystatechange状态变化有所不同。 - 支持更多高级特性: 原生支持流式处理(通过
Response.body)、服务工作线程 (Service Workers) 的请求拦截、以及对 CORS 的更精细控制等。 - 不发送跨域 cookie (默认): 在进行跨域请求时,默认不发送 cookie 和其他用户凭据,这有助于提高安全性,但也需要开发者明确配置
credentials选项来发送。
鉴于这些优势,Fetch API 已经成为现代 Web 开发进行网络请求的首选。
二、Fetch API 的基本用法
Fetch API 的最基本用法非常简单,只需要调用 fetch() 函数,传入要请求的 URL 即可:
“`javascript
fetch(‘https://api.example.com/data’)
.then(response => {
// response 对象代表了整个响应(包括头部信息,但不包括响应主体)
console.log(‘Response received:’, response);
// 检查响应状态码是否表示成功(200-299)
if (!response.ok) {
// 如果不是成功的状态码,抛出错误,Promise 将被拒绝 (reject)
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析响应主体为 JSON。这是一个异步操作,返回一个新的 Promise
return response.json();
})
.then(data => {
// 成功解析 JSON 数据后,在这里处理数据
console.log(‘Data received:’, data);
})
.catch(error => {
// 处理任何在 fetch 或解析过程中发生的错误(包括网络错误和上面抛出的 HTTP 错误)
console.error(‘There was a problem with the fetch operation:’, error);
});
“`
这个例子展示了 Fetch API 的两个关键点:
fetch()函数返回一个 Promise,该 Promise 在接收到响应头部时被解决。- 解决后的 Promise 的值是一个
Response对象。这个Response对象包含响应的状态码、头部等信息。要获取响应的主体内容(如 JSON、文本),需要调用Response对象上的异步方法(如json(),text(),blob()等),这些方法也会返回 Promise。
因此,Fetch 请求通常是一个两步过程:
- 调用
fetch(url, options)发起请求,获取代表响应头部的 Promise。 - 在第一个 Promise 解决后,调用
response.json(),response.text()等方法解析响应主体,获取代表响应主体内容的 Promise。
三、使用 async/await 简化 Fetch 代码
Fetch API 天然支持 Promise,这使得它与 async/await 语法结合使用时异常强大,可以将异步代码写得像同步代码一样直观。上面的例子使用 async/await 可以重写如下:
“`javascript
async function fetchData() {
try {
const response = await fetch(‘https://api.example.com/data’);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Data received:', data);
} catch (error) {
console.error(‘There was a problem with the fetch operation:’, error);
}
}
fetchData();
“`
使用 async/await 后,代码流程更加清晰,更接近我们编写同步代码的习惯,极大地提高了可读性和可维护性。在实际开发中,强烈推荐使用 async/await 来处理 Fetch 请求。
四、配置请求:options 对象详解
fetch() 函数的第二个参数是一个可选的 options 对象,用于配置请求的各种属性,如 HTTP 方法、请求头、请求体、缓存策略等。
javascript
fetch(url, options);
options 对象可以包含以下常用属性:
-
method: HTTP 请求方法,如'GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'。默认值是'GET'。
javascript
method: 'POST' -
headers: 请求头。可以是一个Headers对象,也可以是一个普通的对象字面量。键值对表示请求头名称和对应的值。设置Content-Type对于发送请求体(尤其是 POST 或 PUT 请求)非常重要。
javascript
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token_here'
}
// 或者使用 Headers 对象
// const myHeaders = new Headers();
// myHeaders.append('Content-Type', 'application/json');
// headers: myHeaders -
body: 请求体。用于'POST','PUT','PATCH'等方法发送数据。可以是string,Blob,BufferSource,FormData,URLSearchParams,ReadableStream等类型。- 发送 JSON 数据时,需要先使用
JSON.stringify()转换,并设置Content-Type为application/json。 - 发送表单数据时,可以使用
FormData对象,此时通常无需手动设置Content-Type,浏览器会自动设置一个带有boundary的multipart/form-data。
“`javascript
// 发送 JSON 数据
body: JSON.stringify({ username: ‘example’, password: ‘password’ })
// 发送表单数据
// const formData = new FormData();
// formData.append(‘username’, ‘example’);
// formData.append(‘password’, ‘password’);
// body: formData
“` - 发送 JSON 数据时,需要先使用
-
mode: 请求模式,用于控制跨域请求的行为。'cors'(默认): 允许跨域请求,遵循 CORS 协议。服务器需要返回相应的 CORS 头(如Access-Control-Allow-Origin)。'no-cors': 尝试向其他源发起请求,但不会发送一些敏感头部(如用户凭据),并且响应对象是“不透明”的 (Opaque),无法访问其状态、头部、主体等信息,主要用于发送跟踪 ping 或向其他域的服务发送简单请求而不关心响应内容。只能使用'GET','HEAD','POST'方法,且头部受到严格限制。'same-origin': 只允许同源请求,如果请求不同源,会直接失败。'navigate': 用于导航请求(如用户点击链接或提交表单),只能用于顶级导航。
-
credentials: 凭据策略,用于控制是否发送 cookie、HTTP Basic Auth 等用户凭据。'omit'(默认): 不发送任何凭据。'same-origin': 只在同源请求时发送凭据。'include': 始终发送凭据,即使是跨域请求。跨域发送凭据需要服务器返回Access-Control-Allow-Credentials: true头部。
-
cache: 缓存策略。控制请求如何与 HTTP 缓存交互。'default'(默认): 遵循浏览器和服务器的缓存协议。'no-store': 不读取缓存,不写入缓存。'reload': 跳过缓存,强制从网络获取资源,但会更新缓存。'no-cache': 不读取缓存,但会向服务器发送条件式请求 (If-None-Match, If-Modified-Since),如果资源未改变则返回 304,否则从网络获取并更新缓存。'force-cache': 强制使用缓存,即使缓存过期,除非没有缓存。'only-if-cached': 只在缓存中有匹配项时使用缓存,否则失败。只能用于'same-origin'模式。
-
referrer: 设置请求的Referer头部。可以是'no-referrer','client', 或一个 URL 字符串。 referrerPolicy: 控制Referer头部信息的策略。例如'no-referrer','same-origin','strict-origin','unsafe-url'等。integrity: 用于子资源完整性 (Subresource Integrity, SRI) 检查,验证获取的资源是否被篡改。通常用于<script>和<link>标签。keepalive: 一个布尔值,当页面关闭或卸载时,允许浏览器继续发送请求。主要用于发送分析数据或日志,保证请求在页面卸载前发送成功。通常与navigator.sendBeacon()配合使用,Fetch API 支持此选项提供了另一种选择。signal: 用于中止 (abort) 请求。与AbortController结合使用。
五、处理响应:Response 对象详解
当 fetch() 返回的 Promise 解决后,我们得到一个 Response 对象。这个对象包含了关于响应的各种信息,并提供方法来访问响应的主体。
Response 对象的重要属性:
ok: 一个布尔值,表示响应的状态码是否在 200-299 范围内(成功)。这是检查 HTTP 状态是否成功的常用方法。status: 响应的 HTTP 状态码(如 200, 404, 500)。statusText: 响应状态码对应的文本信息(如 “OK”, “Not Found”, “Internal Server Error”)。headers: 一个Headers对象,包含了响应的所有头部信息。可以通过response.headers.get('Content-Type')等方法访问。url: 响应的最终 URL,考虑到重定向。type: 响应的类型,如'basic','cors','opaque','error'.redirected: 一个布尔值,表示响应是否经过了重定向。body: 一个ReadableStream对象,表示响应的主体。这允许流式读取数据,但通常我们使用更便捷的方法来获取主体内容。bodyUsed: 一个布尔值,表示响应的主体是否已经被读取过。响应主体只能被读取一次。
Response 对象的重要方法(用于解析响应主体,这些方法都返回 Promise):
json(): 将响应主体解析为 JSON 对象。text(): 将响应主体解析为纯文本字符串。blob(): 将响应主体解析为Blob对象(用于处理二进制数据,如图片)。arrayBuffer(): 将响应主体解析为ArrayBuffer(另一种处理二进制数据的方式)。formData(): 将响应主体解析为FormData对象(用于处理multipart/form-data或application/x-www-form-urlencoded格式的响应)。
注意: 这些方法只能调用一次。如果多次调用,会抛出错误。
六、错误处理
Fetch API 的错误处理分为两种类型:
- 网络错误: DNS 查询失败、连接被拒绝、网络中断等导致请求未能成功发出的错误。这些错误会使得
fetch()返回的 Promise 被拒绝 (reject)。使用.catch()或try...catch来捕获。 - HTTP 错误: 请求成功发出并收到了响应,但响应的状态码表示错误(如 404 Not Found, 500 Internal Server Error)。Fetch API 不会因为这些状态码而拒绝 Promise。Promise 仍然会被解决 (resolve),你需要在
.then()或await之后检查response.ok或response.status来判断请求是否在逻辑上成功,并手动抛出错误以便被.catch()捕获。
一个健壮的 Fetch 错误处理应该同时考虑这两种情况:
“`javascript
async function fetchWithErrorHandling(url, options) {
try {
const response = await fetch(url, options);
// 检查 HTTP 状态码是否成功
if (!response.ok) {
// 获取错误信息(如果服务器提供了)
// 注意:这里的 response.json() 也可能失败,需要额外的try...catch
let errorMsg = `HTTP error! status: ${response.status}`;
try {
const errorBody = await response.json();
if (errorBody && errorBody.message) {
errorMsg = `HTTP error! status: ${response.status}, message: ${errorBody.message}`;
} else if (response.statusText) {
errorMsg = `HTTP error! status: ${response.status}, text: ${response.statusText}`;
}
} catch (parseError) {
// 无法解析错误响应体,使用默认错误信息
console.warn('Could not parse error response body:', parseError);
}
// 抛出自定义错误,包含状态码等信息
const httpError = new Error(errorMsg);
httpError.status = response.status;
httpError.response = response; // 可选:保留原始响应对象
throw httpError;
}
// 成功时解析并返回数据
const data = await response.json();
return data;
} catch (error) {
// 捕获网络错误或上面抛出的 HTTP 错误
console.error(‘Fetch operation failed:’, error);
// 可以根据 error 的类型或属性进行更精细的处理
if (error.message.startsWith(‘HTTP error!’)) {
console.log(‘Handled an HTTP error.’);
} else {
console.log(‘Handled a network error.’);
}
// 可以选择重新抛出错误或者返回一个特定的错误状态
throw error; // 向上层调用者传递错误
}
}
// 调用这个封装函数
fetchWithErrorHandling(‘https://api.example.com/nonexistent’) // 模拟 404 错误
.catch(err => {
console.log(‘Caught error from wrapper:’, err);
if (err.status === 404) {
console.log(‘Specifically handled 404.’);
}
});
fetchWithErrorHandling(‘https://invalid-url-that-does-not-exist.com’) // 模拟网络错误
.catch(err => {
console.log(‘Caught error from wrapper:’, err);
// 网络错误没有 status 属性
if (!err.status) {
console.log(‘Specifically handled network error.’);
}
});
“`
通过这种封装,我们可以统一处理两种类型的错误,并提供更详细的错误信息。
七、中止 Fetch 请求:AbortController
在某些场景下,我们可能需要在请求完成之前取消它,例如用户在搜索框快速输入时取消前一个未完成的搜索请求,或者组件卸载时取消未完成的请求以避免内存泄漏。Fetch API 结合 AbortController 提供了中止请求的能力。
AbortController 是一个简单的 API,它包含一个 signal 属性和一个 abort() 方法。我们将 signal 传递给 Fetch 请求的 options 对象的 signal 属性。当调用 controller.abort() 方法时,与该信号关联的 Fetch 请求就会被中止。中止的 Fetch Promise 会被拒绝 (reject),并抛出一个 AbortError。
“`javascript
const controller = new AbortController();
const signal = controller.signal;
const fetchTimeout = setTimeout(() => {
controller.abort(); // 10秒后中止请求
}, 10000);
fetch(‘https://api.example.com/large-data’, { signal: signal })
.then(response => {
clearTimeout(fetchTimeout); // 请求成功,清除超时计时器
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
return response.json();
})
.then(data => {
console.log(‘Data received:’, data);
})
.catch(error => {
clearTimeout(fetchTimeout); // 无论成功失败,都清除计时器
if (error.name === ‘AbortError’) {
console.log(‘Fetch request was aborted.’);
} else {
console.error(‘Fetch error:’, error);
}
});
// 可以在其他地方根据用户操作等调用 controller.abort()
// 例如:用户点击了取消按钮
// document.getElementById(‘cancelButton’).addEventListener(‘click’, () => {
// controller.abort();
// });
使用 `async/await` 的写法:javascript
async function fetchWithAbort() {
const controller = new AbortController();
const signal = controller.signal;
const fetchTimeout = setTimeout(() => {
controller.abort();
}, 10000);
try {
const response = await fetch(‘https://api.example.com/large-data’, { signal: signal });
clearTimeout(fetchTimeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Data received:', data);
} catch (error) {
clearTimeout(fetchTimeout);
if (error.name === ‘AbortError’) {
console.log(‘Fetch request was aborted.’);
} else {
console.error(‘Fetch error:’, error);
}
}
}
fetchWithAbort();
``AbortController` 是中止 Fetch 请求的标准方式,非常实用。
八、Fetch API 的局限性与高级话题
虽然 Fetch API 强大且灵活,但它也有一些局限性,或者说某些功能不如 XHR 直观:
- 没有内置的请求超时控制: 需要结合
AbortController和setTimeout手动实现超时逻辑(如上面的例子所示)。 - 没有内置的请求进度监听: 对于文件上传或下载,Fetch API 原生不易监听进度。虽然可以通过读取
response.body的ReadableStream并结合TransformStream来实现,但这比 XHR 的onprogress事件要复杂得多。 - 不发送跨域 cookie 默认: 如前所述,需要显式设置
credentials: 'include'。 - 不会对非 2xx 状态码抛出错误: 需要手动检查
response.ok。
高级话题简述:
-
Request 对象: 可以先创建一个
Request对象,再将其作为参数传递给fetch()。这在需要复制或修改请求时很有用。
“`javascript
const myRequest = new Request(‘https://api.example.com/data’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ name: ‘test’ })
});fetch(myRequest)
.then(…)
* **Headers 对象:** 提供了方便的方法来操作请求头和响应头。javascript
const myHeaders = new Headers();
myHeaders.append(‘Content-Type’, ‘application/json’);
myHeaders.set(‘X-Custom-Header’, ‘fetch-demo’);
myHeaders.has(‘Content-Type’); // true
myHeaders.delete(‘X-Custom-Header’);fetch(url, { headers: myHeaders });
``ReadableStream
* **流式处理 ():** 对于非常大的响应,可以直接访问response.body获取一个ReadableStream`,逐块读取数据而无需等待整个响应下载完成。这在处理大型文件或持续更新的数据流时非常有用。
* 服务工作线程 (Service Workers) 中的应用: Fetch API 是 Service Workers 的核心,用于拦截和处理网络请求,实现离线访问、缓存策略、请求修改等功能。
九、Fetch vs XMLHttpRequest 总结比较
| 特性 | Fetch API | XMLHttpRequest (XHR) |
|---|---|---|
| 异步模式 | 基于 Promise | 基于事件回调 (onreadystatechange, onload等) |
| API 设计 | 简洁,更符合 HTTP 概念 (Request/Response) |
冗余,方法和属性分散 |
| 语法 | 清晰,尤其配合 async/await |
相对繁琐,易形成回调嵌套 |
| 错误处理 | 网络错误 Promise rejected;HTTP 错误需手动检查 response.ok 并抛出 |
网络错误/部分 HTTP 错误触发 onerror;其他状态需检查 status |
| 响应主体 | 通过异步方法 (json(), text()等) 读取一次 |
通过 responseText, responseXML 等属性读取,可多次读取 |
| 流式处理 | 原生支持 (response.body 是 ReadableStream) |
不原生支持,实现复杂 |
| 凭据 (Cookie) | 默认不发送跨域凭据,需显式设置 credentials |
默认发送同源和部分跨域凭据(取决于浏览器策略) |
| 超时控制 | 无内置选项,需结合 AbortController/setTimeout |
有内置 timeout 属性 |
| 上传/下载进度 | 原生不易监听,需处理流 | 有内置 onprogress 事件 |
| 中止请求 | 使用 AbortController |
使用 abort() 方法 |
| 现代特性 | 支持 Service Workers, CORS 精细控制 | 支持较少 |
| 兼容性 | 现代浏览器广泛支持,IE 不支持 | 几乎所有浏览器都支持(包括 IE) |
十、实践建议与代码示例
在实际开发中,为了提高代码的可重用性和健壮性,通常会对 Fetch API 进行封装,构建自己的网络请求工具函数或类。
一个简单的 Fetch 封装函数示例(包含基本错误处理和 JSON 数据处理):
“`javascript
const API_BASE_URL = ‘https://api.example.com’; // 你的 API 基础 URL
async function request(endpoint, options = {}) {
const url = ${API_BASE_URL}${endpoint};
const { method = ‘GET’, headers, body, …restOptions } = options;
const config = {
method,
headers: {
// 默认头部,可以被 options.headers 覆盖
‘Content-Type’: ‘application/json’,
…headers,
},
// 对于 GET 或 HEAD 方法,不应包含 body
body: method === ‘GET’ || method === ‘HEAD’ ? undefined : JSON.stringify(body),
…restOptions, // 允许传递其他 Fetch 选项,如 mode, credentials, signal 等
};
// 移除 body 为 undefined 的情况,fetch 会自动处理
if (config.body === undefined) {
delete config.body;
// 如果没有 body 且 Content-Type 默认设置了 application/json,需要移除
if (config.headers[‘Content-Type’] === ‘application/json’ && (method === ‘GET’ || method === ‘HEAD’)) {
delete config.headers[‘Content-Type’];
// 如果headers对象为空,可以考虑移除 headers 属性
if (Object.keys(config.headers).length === 0) {
delete config.headers;
}
}
}
try {
const response = await fetch(url, config);
if (!response.ok) {
let errorData = null;
try {
// 尝试读取错误响应体,即使是错误状态码,服务器也可能返回 JSON 格式的错误信息
errorData = await response.json();
} catch (parseError) {
// 如果无法解析 JSON,忽略此错误
console.warn(`Could not parse error response body from ${url}:`, parseError);
}
const error = new Error(`HTTP error! status: ${response.status}`);
error.status = response.status;
error.statusText = response.statusText;
error.response = response; // 保存原始响应对象
error.data = errorData; // 保存解析到的错误数据
throw error;
}
// 成功时尝试解析 JSON,如果响应体为空或非 JSON,这里可能会失败
// 根据实际 API 响应决定是否必须解析 JSON
try {
const data = await response.json();
return data; // 返回解析后的数据
} catch (parseError) {
// 如果响应成功但不是 JSON (例如 204 No Content),则返回原始响应对象或 null
console.warn(`Could not parse JSON response from ${url}:`, parseError);
// 根据 API 约定,如果成功但无体,可以返回 response 或 null
return response; // 例如,对于 DELETE 请求,可能只需确认状态码
}
} catch (error) {
// 捕获网络错误或上面抛出的 HTTP 错误
console.error(Request failed for ${url}:, error);
throw error; // 重新抛出以便调用者处理
}
}
// 使用示例:GET 请求
request(‘/users/123’)
.then(user => console.log(‘User data:’, user))
.catch(err => console.error(‘Failed to fetch user:’, err));
// 使用示例:POST 请求
request(‘/posts’, {
method: ‘POST’,
body: { title: ‘New Post’, content: ‘…’ },
headers: { ‘Authorization’: ‘Bearer token’ }
})
.then(newPost => console.log(‘Created post:’, newPost))
.catch(err => console.error(‘Failed to create post:’, err));
// 使用示例:带中止信号的 GET 请求
const controller = new AbortController();
const signal = controller.signal;
request(‘/data?large=true’, { signal: signal })
.then(data => console.log(‘Large data received:’, data))
.catch(err => {
if (err.name === ‘AbortError’) {
console.log(‘Large data fetch aborted.’);
} else {
console.error(‘Failed to fetch large data:’, err);
}
});
// 假设在某个时刻需要中止请求
// controller.abort();
“`
这个封装函数提供了一个更友好的接口,默认处理 JSON,并统一了错误处理。你可以根据项目需求进一步扩展,例如添加拦截器功能、处理文件上传下载进度等。
十一、总结
Fetch API 是 JavaScript 中进行网络请求的现代标准。它基于 Promise,提供了简洁、灵活的 API 设计,并支持许多高级特性。尽管在某些方面(如超时和进度监听)不如 XMLHttpRequest 直观,但其 Promise 特性、与 async/await 的良好集成以及对新 Web 标准的支持,使其成为绝大多数前端网络请求场景的首选。
掌握 Fetch API 是现代 Web 开发者的必备技能。通过深入理解其基本用法、options 对象配置、Response 对象处理、错误处理机制以及 AbortController 等高级特性,你可以更高效、更健壮地进行网络通信,构建功能强大的 Web 应用。
现在,是时候在你的项目中使用 Fetch API 替换掉老旧的 XHR 代码,享受 Promise 带来的便利和代码的清晰性了!