
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: userexample.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 处理其他请求方法与请求体格式除了 JSONfetch可以发送多种格式的数据。发送 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-emailexample.com }) });4. 深入请求配置与高级控制4.1 详解 init 配置对象init对象是fetch的神经中枢理解每个选项的用途至关重要。配置项类型默认值描述与常见用途methodStringGETHTTP 方法GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS。headersObject / Headers{}请求头对象。使用Headers对象或普通对象字面量。Content-Type在这里设置。bodyString / Blob / FormData 等null请求体。GET/HEAD 请求不能有 body。modeStringcors请求模式cors默认允许跨域、no-cors限制性跨域、same-origin仅同源。credentialsStringsame-origin是否发送 cookiesomit不发送、same-origin同源发送、include总是发送用于跨域带认证。cacheStringdefault缓存模式default,no-store,reload,no-cache,force-cache,only-if-cached。redirectStringfollow重定向处理follow自动、error报错、manual手动处理。referrerStringabout:client引用页。referrerPolicyStringstrict-origin-when-cross-origin引用策略。integrityString子资源完整性SRI哈希值用于校验资源完整性。keepaliveBooleanfalse是否允许请求在页面卸载后继续。用于发送分析数据等场景。signalAbortSignalnull用于取消请求的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、默认头、错误处理、认证令牌刷新、请求/响应拦截器等。这会让你的代码更易维护和测试。