深入理解 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 带来的便利和代码的清晰性了!