三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

现代前端必备:深入掌握 Fetch API 从基础到高级实践

现代前端必备:深入掌握 Fetch API 从基础到高级实践

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 请求场景。关键点在于正确设置headersbody

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的神经中枢,理解每个选项的用途至关重要。

配置项类型默认值描述与常见用途
methodString'GET'HTTP 方法:'GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'
headersObject / Headers{}请求头对象。使用Headers对象或普通对象字面量。Content-Type在这里设置。
bodyString / Blob / FormData 等null请求体。GET/HEAD 请求不能有 body。
modeString'cors'请求模式:'cors'(默认,允许跨域)、'no-cors'(限制性跨域)、'same-origin'(仅同源)。
credentialsString'same-origin'是否发送 cookies:'omit'(不发送)、'same-origin'(同源发送)、'include'(总是发送,用于跨域带认证)。
cacheString'default'缓存模式:'default','no-store','reload','no-cache','force-cache','only-if-cached'
redirectString'follow'重定向处理:'follow'(自动)、'error'(报错)、'manual'(手动处理)。
referrerString'about:client'引用页。
referrerPolicyString'strict-origin-when-cross-origin'引用策略。
integrityString子资源完整性(SRI)哈希值,用于校验资源完整性。
keepaliveBooleanfalse是否允许请求在页面卸载后继续。用于发送分析数据等场景。
signalAbortSignalnull用于取消请求的AbortSignal对象。

重点配置实战解析

  1. 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'

  2. mode: 'no-cors':这是一个“受限”模式。在此模式下,你只能发起简单请求(方法限于 GET/POST/HEAD,头信息有限),并且返回的Response是“不透明”的(opaque),你无法读取其状态码、头信息或内容。它通常只用于向不需要响应内容的分析端点发送数据。除非你非常清楚后果,否则不要轻易使用'no-cors'

  3. 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对象时,如果bodyFormData不要设置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不支持直接的超时参数,但我们可以利用AbortControllerPromise.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时,你需要明确以下几点:

  1. 简单请求与预检请求:浏览器将请求分为简单请求和非简单请求。简单请求(如 GET/POST 且使用特定头)会直接发出。非简单请求(如使用了Content-Type: application/json或自定义头)会先发一个OPTIONS方法的预检请求,服务器必须正确响应预检请求,真正的请求才会发出。
  2. 服务器必须配合:跨域成功与否,70% 取决于后端服务器的 CORS 配置。正确的响应头应包括:
    • Access-Control-Allow-Origin: 允许的源(或*,但不能与credentials: include共用)。
    • Access-Control-Allow-Methods: 允许的 HTTP 方法。
    • Access-Control-Allow-Headers: 允许的请求头。
    • Access-Control-Allow-Credentials:true(如果需要带 Cookie)。
  3. 本地开发代理:在开发环境中,最省心的方式是使用开发服务器(如 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: trueAllow-Origin不是*
请求超时无响应网络问题、服务器处理慢、未设置超时。1. 使用AbortController实现超时控制。
2. 检查网络连接和服务器状态。
3. 对于长时间操作,考虑实现轮询或 WebSocket。
POST请求成功,但服务器收不到数据1.body未正确序列化。
2.Content-Type头不匹配。
1. 确保body是字符串(JSON.stringify)、FormDataURLSearchParams
2. 检查headers中的Content-Type是否与body格式匹配(application/json,multipart/form-data等)。
在 React/Vue 组件中,组件卸载后setState警告请求返回后,组件已卸载。使用AbortController在组件卸载时取消未完成的请求。在 React 的useEffect清理函数中调用controller.abort()

6.4 性能优化与最佳实践心得

  1. 复用连接:浏览器会自动为同源请求复用 HTTP/1.1 的连接或 HTTP/2 的多路复用,fetch本身无需特殊配置。但保持合理的并发请求数,避免短时间内发起大量请求阻塞浏览器。
  2. 请求去重:对于相同的、非实时性的数据请求,可以考虑在前端做简单的缓存(如用一个 Map 存储 URL 和对应的 Promise),在同一个页面生命周期内避免重复请求。
  3. 合理使用缓存策略:根据数据特性设置cache选项。静态资源可设为'force-cache',实时数据用'no-cache''no-store'
  4. 压缩与分片:确保服务器启用了 Gzip/Brotli 压缩。对于列表数据,务必使用分页,不要一次性拉取海量数据。
  5. 与 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; } }
  6. 封装与抽象:在实际项目中,不要在每个组件里直接写fetch。应该封装一个统一的 HTTP 客户端模块,集中处理基础 URL、默认头、错误处理、认证令牌刷新、请求/响应拦截器等。这会让你的代码更易维护和测试。
← 返回列表