1. 项目概述:为什么今天还要学 fetch?
如果你在2024年还在用着XMLHttpRequest或者依赖着$.ajax来处理网络请求,那感觉就像是在智能手机时代还在用着功能机发短信——不是不能用,只是有点“复古”。fetchAPI 作为现代浏览器原生提供的网络请求接口,从 ES6 时代走来,已经成为前端异步通信的事实标准。我刚开始接触它时,也觉得不就是个发请求的玩意儿,用axios封装好的不香吗?但真正深入项目,尤其是需要处理流式数据、精细控制请求生命周期或构建无第三方依赖的轻量级应用时,你会发现直接驾驭fetch能带来极大的灵活性和性能优势。
简单说,fetch()提供了一个更强大、更灵活、基于 Promise 的机制,用来获取网络资源。它替代了传统的XMLHttpRequest,语法更简洁,与现代 JavaScript 的异步编程模式(async/await)结合得天衣无缝。无论是调用 RESTful API、上传文件、还是处理服务器推送的数据流,fetch都是你工具箱里的核心工具。这个系列的第一篇,我们就从最基础的用法开始,把它掰开揉碎了讲清楚,让你能稳稳地上手,避开我当年踩过的那些坑。
2. 核心概念与基础语法拆解
2.1 fetch() 函数的基本形态
fetch()的核心语法非常简单:fetch(resource [, init])。它返回一个 Promise 对象,这个 Promise 会在请求接收到响应头(Response Headers)后解析(resolve),而不是等到整个响应体下载完成。这是理解fetch行为的关键第一点。
resource: 通常是一个字符串,代表你想获取资源的 URL。也可以是一个Request对象实例,这提供了更高的配置灵活性。init(可选): 一个配置对象,用来定制你的 HTTP 请求。这是fetch强大之处所在,我们可以在这里设置请求方法、头信息、请求体、模式(如跨域设置)等。
一个最基础的 GET 请求看起来是这样的:
fetch('https://api.example.com/data') .then(response => { // 注意:此时 Promise 已解决,但响应体可能还未完全加载 console.log(response.ok); // 检查 HTTP 状态码是否在 200-299 范围内 return response.json(); // 这是一个异步操作,返回另一个 Promise }) .then(data => { // 这里才真正拿到解析后的 JSON 数据 console.log(data); }) .catch(error => { // 捕获网络错误或请求未能发起的错误 console.error('Fetch error:', error); });第一个关键点:fetch只在网络故障或请求无法完成时(例如,域名解析失败、跨域被浏览器拒绝)才会拒绝(reject)返回的 Promise。对于 HTTP 状态码如 404、500 等,fetch的 Promise依然会正常解析(resolve)。你必须通过检查response.ok属性或response.status来手动判断业务逻辑上的成功与否。这是我见过新手最常掉进去的坑,以为 404 了就会进.catch。
2.2 理解 Response 对象
fetch()返回的 Promise 解析后,你会得到一个Response对象。这个对象是响应信息的容器,它本身并不直接包含数据。你需要调用Response对象上的方法来获取实际的数据体。这些方法都是异步的,返回另一个 Promise。
常用的数据提取方法有:
.json(): 将响应体解析为 JSON 对象。如果响应不是有效的 JSON,会抛出错误。.text(): 将响应体解析为纯文本字符串。.blob(): 将响应体解析为Blob(二进制大对象)对象,常用于处理图片、文件等。.arrayBuffer(): 将响应体解析为ArrayBuffer(原始二进制数据缓冲区),用于更底层的二进制操作。.formData(): 将响应体解析为FormData对象。
重要原则:一个Response的 body 只能被读取一次。如果你调用了response.json(),就不能再调用response.text()了。如果你需要多次使用响应体内容,必须在第一次读取后使用.clone()方法克隆一份响应。
fetch('https://api.example.com/data') .then(response => { // 错误示例:试图读取两次 // const jsonPromise = response.json(); // const textPromise = response.text(); // 这里会报错 // 正确做法:克隆响应 const responseClone = response.clone(); const jsonPromise = response.json(); const textPromise = responseClone.text(); return Promise.all([jsonPromise, textPromise]); }) .then(([jsonData, textData]) => { console.log(jsonData, textData); });3. 发起不同类型的请求
3.1 配置 GET 与 POST 请求
默认情况下,fetch()发起的是 GET 请求。要发起其他方法的请求,需要在init配置对象中指定method。
GET 请求(带查询参数): 对于 GET 请求,参数通常附加在 URL 上。建议使用URLSearchParams来构建查询字符串,它能自动处理编码。
const params = new URLSearchParams({ page: 1, limit: 20, keyword: '前端开发' }); const url = `https://api.example.com/search?${params.toString()}`; fetch(url) .then(response => response.json()) .then(data => console.log(data));POST 请求(发送 JSON 数据): 这是最常见的非 GET 请求场景。关键点在于正确设置headers和body。
const userData = { username: 'newUser', email: 'user@example.com' }; fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', // 必须明确指定 // 可以添加其他头,如认证令牌 // 'Authorization': `Bearer ${token}` }, body: JSON.stringify(userData) // 必须将对象序列化为 JSON 字符串 }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => console.log('User created:', data)) .catch(error => console.error('Error:', error));注意:当你设置
body时,fetch会自动将method设置为'POST'(如果未显式指定的话)。但为了代码清晰,建议总是显式写明method。
3.2 处理其他请求方法与请求体格式
除了 JSON,fetch可以发送多种格式的数据。
发送 FormData(常用于文件上传或模拟表单提交):
const formData = new FormData(); formData.append('username', 'john'); formData.append('avatar', fileInput.files[0]); // 假设有一个文件输入框 fetch('https://api.example.com/profile', { method: 'POST', body: formData // 注意:当 body 是 FormData 时,浏览器会自动设置 `Content-Type` 为 `multipart/form-data`,并带上边界,所以通常不需要手动设置 headers });发送 URL 编码数据(application/x-www-form-urlencoded): 有些老式 API 可能期望这种格式。
const urlEncodedData = new URLSearchParams(); urlEncodedData.append('grant_type', 'password'); urlEncodedData.append('username', 'user'); urlEncodedData.append('password', 'pass'); fetch('https://api.example.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: urlEncodedData });PUT、PATCH、DELETE 请求: 配置方式与 POST 类似,只需更改method字段。
// DELETE 请求 fetch(`https://api.example.com/users/123`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }) .then(response => { if (response.status === 204) { // 204 No Content 是删除成功的常见状态码 console.log('Deleted successfully'); } }); // PATCH 请求(部分更新) fetch(`https://api.example.com/users/123`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'new-email@example.com' }) });4. 深入请求配置与高级控制
4.1 详解 init 配置对象
init对象是fetch的神经中枢,理解每个选项的用途至关重要。
| 配置项 | 类型 | 默认值 | 描述与常见用途 |
|---|---|---|---|
method | String | 'GET' | HTTP 方法:'GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'。 |
headers | Object / Headers | {} | 请求头对象。使用Headers对象或普通对象字面量。Content-Type在这里设置。 |
body | String / Blob / FormData 等 | null | 请求体。GET/HEAD 请求不能有 body。 |
mode | String | 'cors' | 请求模式:'cors'(默认,允许跨域)、'no-cors'(限制性跨域)、'same-origin'(仅同源)。 |
credentials | String | 'same-origin' | 是否发送 cookies:'omit'(不发送)、'same-origin'(同源发送)、'include'(总是发送,用于跨域带认证)。 |
cache | String | 'default' | 缓存模式:'default','no-store','reload','no-cache','force-cache','only-if-cached'。 |
redirect | String | 'follow' | 重定向处理:'follow'(自动)、'error'(报错)、'manual'(手动处理)。 |
referrer | String | 'about:client' | 引用页。 |
referrerPolicy | String | 'strict-origin-when-cross-origin' | 引用策略。 |
integrity | String | 子资源完整性(SRI)哈希值,用于校验资源完整性。 | |
keepalive | Boolean | false | 是否允许请求在页面卸载后继续。用于发送分析数据等场景。 |
signal | AbortSignal | null | 用于取消请求的AbortSignal对象。 |
重点配置实战解析:
credentials: 'include':这是跨域请求携带 Cookie 或 HTTP 认证信息的关键。如果你的前端(例如http://localhost:3000)需要调用后端 API(例如https://api.yoursite.com)且需要基于 Session 或 Cookie 的认证,那么前后端都需要配置。后端需要设置Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能为通配符*,必须是明确的域名。前端fetch则需要设置credentials: 'include'。mode: 'no-cors':这是一个“受限”模式。在此模式下,你只能发起简单请求(方法限于 GET/POST/HEAD,头信息有限),并且返回的Response是“不透明”的(opaque),你无法读取其状态码、头信息或内容。它通常只用于向不需要响应内容的分析端点发送数据。除非你非常清楚后果,否则不要轻易使用'no-cors'。cache控制:对于动态数据接口,通常使用'no-cache'或'no-store'来避免浏览器缓存干扰,确保获取最新数据。'no-cache'会向服务器验证缓存有效性,'no-store'则完全不使用缓存。
4.2 使用 Headers 对象管理请求头
虽然你可以用一个普通对象设置headers,但使用Headers对象可以提供更规范的操作。
const myHeaders = new Headers(); myHeaders.append('Content-Type', 'application/json'); myHeaders.append('X-Custom-Header', 'value'); // 或者通过可迭代对象初始化 const headers = new Headers({ 'Content-Type': 'application/json', 'X-Custom-Header': 'value' }); // 检查、获取、设置、删除 headers.has('Content-Type'); // true headers.get('Content-Type'); // 'application/json' headers.set('X-Custom-Header', 'new-value'); headers.delete('X-Custom-Header'); fetch(url, { method: 'POST', headers: myHeaders, // 直接使用 Headers 对象 body: JSON.stringify(data) });一个关于Content-Type的常见坑:当你手动设置headers对象时,如果body是FormData,不要设置Content-Type!浏览器会自动为你设置正确的multipart/form-data并带上边界(boundary)。如果你手动设置了一个错误的Content-Type,服务器可能无法正确解析你的表单数据。
5. 错误处理与超时控制
5.1 全面的错误处理策略
如前所述,fetch不会因为 HTTP 错误状态码(4xx, 5xx)而进入catch。因此,一个健壮的错误处理流程是必须的。
async function fetchWithErrorHandling(url, options = {}) { try { const response = await fetch(url, options); // 首先检查网络响应是否成功(状态码 200-299) if (!response.ok) { // 尝试获取服务器返回的错误信息(可能是 JSON 或文本) let errorMessage = `HTTP Error ${response.status}`; try { // 假设服务器错误响应是 JSON 格式 const errorBody = await response.json(); errorMessage = errorBody.message || errorMessage; } catch (e) { // 如果响应不是 JSON,尝试读取为文本 const text = await response.text(); if (text) errorMessage = `${errorMessage}: ${text.substring(0, 100)}`; } // 抛出一个包含状态和信息的错误,方便上层捕获 throw new Error(errorMessage, { cause: { status: response.status, response } }); } // 响应成功,解析数据(这里假设是 JSON) return await response.json(); } catch (error) { // 这里捕获的可能是网络错误、解析错误或我们上面抛出的 HTTP 错误 console.error('Fetch operation failed:', error); // 可以根据错误类型进行不同的 UI 反馈或重试逻辑 if (error.name === 'TypeError') { // 很可能是网络错误(如 CORS 失败、无法连接) console.error('Network or CORS error detected.'); } // 将错误重新抛出,或返回一个表示失败的统一结构 throw error; } } // 使用示例 fetchWithErrorHandling('https://api.example.com/data') .then(data => console.log('Success:', data)) .catch(error => { // 在这里进行最终的 UI 错误展示 alert(`请求失败: ${error.message}`); });5.2 实现请求超时与取消
原生fetch不支持直接的超时参数,但我们可以利用AbortController和Promise.race()来实现。
使用 AbortController 取消请求: 这是现代浏览器支持的优雅取消机制。
// 创建一个 AbortController 实例 const controller = new AbortController(); const signal = controller.signal; // 设置一个 5 秒后超时的定时器 const timeoutId = setTimeout(() => { controller.abort(); // 触发取消 console.log('Request timed out'); }, 5000); fetch('https://api.example.com/slow-endpoint', { signal: signal // 将 signal 传入 fetch 配置 }) .then(response => { clearTimeout(timeoutId); // 请求成功,清除超时定时器 if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }) .then(data => console.log(data)) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { console.error('Request was aborted due to timeout.'); } else { console.error('Other fetch error:', error); } });结合超时与 AbortController 的通用封装:
async function fetchWithTimeout(resource, options = {}, timeout = 8000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const fetchOptions = { ...options, signal: controller.signal }; try { const response = await fetch(resource, fetchOptions); clearTimeout(id); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); // 根据实际情况调整解析方法 } catch (error) { clearTimeout(id); if (error.name === 'AbortError') { throw new Error(`Request timed out after ${timeout}ms`); } throw error; // 重新抛出其他错误 } } // 使用 fetchWithTimeout('https://api.example.com/data', {}, 5000) .then(data => console.log(data)) .catch(err => console.error(err.message));6. 实战技巧与常见问题排查
6.1 处理跨域请求的陷阱
跨域是前端开发永恒的“话题”。使用fetch时,你需要明确以下几点:
- 简单请求与预检请求:浏览器将请求分为简单请求和非简单请求。简单请求(如 GET/POST 且使用特定头)会直接发出。非简单请求(如使用了
Content-Type: application/json或自定义头)会先发一个OPTIONS方法的预检请求,服务器必须正确响应预检请求,真正的请求才会发出。 - 服务器必须配合:跨域成功与否,70% 取决于后端服务器的 CORS 配置。正确的响应头应包括:
Access-Control-Allow-Origin: 允许的源(或*,但不能与credentials: include共用)。Access-Control-Allow-Methods: 允许的 HTTP 方法。Access-Control-Allow-Headers: 允许的请求头。Access-Control-Allow-Credentials:true(如果需要带 Cookie)。
- 本地开发代理:在开发环境中,最省心的方式是使用开发服务器(如 Vite、Webpack DevServer)的代理功能,将 API 请求代理到同源地址,从而绕过浏览器的 CORS 限制。这只是一个开发便利,上线后仍需处理真正的跨域。
6.2 处理大文件与流式响应
对于非常大的响应(如你提到的超过 500MB 的 JSON),一次性加载到内存并调用.json()会导致内存溢出。fetch的优势在于其响应体(response.body)是一个可读流,我们可以分块处理。
async function processLargeJsonStream(url) { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; // value 是一个 Uint8Array 块 buffer += decoder.decode(value, { stream: true }); // 尝试从 buffer 中解析出完整的 JSON 行或对象(取决于你的数据格式) // 假设是每行一个 JSON 对象(NDJSON 格式) const lines = buffer.split('\n'); buffer = lines.pop(); // 最后一行可能不完整,留回缓冲区 for (const line of lines) { if (line.trim()) { try { const obj = JSON.parse(line); // 处理每一个解析出的对象,而不是等待全部完成 console.log('Processed item:', obj.id); } catch (e) { console.error('Error parsing line:', line, e); } } } } // 处理缓冲区中剩余的数据 if (buffer.trim()) { const finalObj = JSON.parse(buffer); console.log('Final item:', finalObj.id); } } finally { reader.releaseLock(); } }对于非流式 JSON,如果服务器不支持分块传输,你仍然可能面临内存问题。此时,唯一的办法是要求后端 API 提供分页或数据分片功能。
6.3 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
请求成功但response.json()报错 | 1. 响应体不是有效的 JSON。 2. 响应体为空。 | 1. 先用response.text()查看原始返回内容。2. 检查服务器 API 是否返回了 JSON 格式,特别是错误时可能返回 HTML 或纯文本。 3. 使用 try...catch包裹.json()调用。 |
| 跨域请求失败,控制台报 CORS 错误 | 服务器未正确配置 CORS 响应头。 | 1. 检查网络面板,查看OPTIONS预检请求或实际请求的响应头。2. 确认后端已正确设置 Access-Control-Allow-Origin等头信息。3. 开发环境使用代理解决。 |
| 请求未发送 Cookie 或认证信息 | credentials选项未设置或设置为'omit'。 | 1. 在fetch配置中设置credentials: 'include'。2. 确保服务器 Access-Control-Allow-Credentials: true且Allow-Origin不是*。 |
| 请求超时无响应 | 网络问题、服务器处理慢、未设置超时。 | 1. 使用AbortController实现超时控制。2. 检查网络连接和服务器状态。 3. 对于长时间操作,考虑实现轮询或 WebSocket。 |
POST请求成功,但服务器收不到数据 | 1.body未正确序列化。2. Content-Type头不匹配。 | 1. 确保body是字符串(JSON.stringify)、FormData或URLSearchParams。2. 检查 headers中的Content-Type是否与body格式匹配(application/json,multipart/form-data等)。 |
在 React/Vue 组件中,组件卸载后setState警告 | 请求返回后,组件已卸载。 | 使用AbortController在组件卸载时取消未完成的请求。在 React 的useEffect清理函数中调用controller.abort()。 |
6.4 性能优化与最佳实践心得
- 复用连接:浏览器会自动为同源请求复用 HTTP/1.1 的连接或 HTTP/2 的多路复用,
fetch本身无需特殊配置。但保持合理的并发请求数,避免短时间内发起大量请求阻塞浏览器。 - 请求去重:对于相同的、非实时性的数据请求,可以考虑在前端做简单的缓存(如用一个 Map 存储 URL 和对应的 Promise),在同一个页面生命周期内避免重复请求。
- 合理使用缓存策略:根据数据特性设置
cache选项。静态资源可设为'force-cache',实时数据用'no-cache'或'no-store'。 - 压缩与分片:确保服务器启用了 Gzip/Brotli 压缩。对于列表数据,务必使用分页,不要一次性拉取海量数据。
- 与 async/await 优雅结合:使用
async/await可以让异步代码更清晰。但要注意错误处理,最好用try...catch包裹。async function getUserData(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error(`Failed to fetch user: ${response.status}`); return await response.json(); } catch (error) { console.error(`Error fetching user ${userId}:`, error); // 返回一个兜底值或抛出,取决于你的错误处理策略 return null; } } - 封装与抽象:在实际项目中,不要在每个组件里直接写
fetch。应该封装一个统一的 HTTP 客户端模块,集中处理基础 URL、默认头、错误处理、认证令牌刷新、请求/响应拦截器等。这会让你的代码更易维护和测试。