1. 项目概述:为什么今天还要学Fetch?
如果你是一名前端开发者,或者正在学习JavaScript,那么“网络请求”这个概念你一定绕不开。从早期的XMLHttpRequest到后来的jQuery.ajax,再到如今几乎成为现代JavaScript异步请求代名词的fetch,我们与服务器交互的方式一直在演进。今天,我们就来深入聊聊fetch的基础用法。你可能会想,这都什么年代了,fetch不是早就有了吗?没错,但根据我这些年的观察,很多开发者,包括一些有一定经验的,对fetch的理解和使用仍然停留在“能用就行”的层面,对其背后的设计理念、错误处理机制以及一些高级特性知之甚少。这就像你有一辆性能车,却只会用D挡开一样,浪费了太多潜力。
fetchAPI提供了一个更强大、更灵活、基于Promise的接口,用于获取资源(包括跨网络)。它取代了老旧且笨拙的XMLHttpRequest,成为了现代Web开发的标配。但它的“简单”只是表象,其设计哲学和细节处理,恰恰是区分普通使用者和精通者的关键。本系列的第一篇,我们将从最基础、最核心的用法开始,帮你建立一个坚实、正确的认知起点,避免日后踩坑。
2. Fetch API的核心设计哲学与基础结构
2.1 从“回调地狱”到“Promise链”
在fetch出现之前,我们主要依赖XMLHttpRequest或第三方库(如jQuery)。XMLHttpRequest基于事件和回调函数,代码结构容易陷入所谓的“回调地狱”,嵌套层级深,可读性和可维护性都较差。
fetch的设计核心是Promise。Promise是ES6引入的用于处理异步操作的标准化对象,它代表一个尚未完成但预期将来会完成的操作。fetch()函数调用后,会立即返回一个Promise对象。这个Promise在请求完成(无论成功或失败)后,会进入“已敲定”状态,并触发相应的回调(.then()或.catch())。
这种链式调用的方式,让异步代码的流程变得清晰、线性,更符合人类的同步思维习惯。这也是fetchAPI最根本的进步之一。
2.2 Fetch请求的生命周期与Response对象
一个完整的fetch请求,其生命周期可以简化为以下几步:
- 调用
fetch(url, options):发起请求。 - 返回Promise:这个Promise会在网络请求完成(收到HTTP响应头)时被解决(resolve)。注意,这里“完成”指的是HTTP事务完成,而不是响应体被完全读取。
- 处理Response对象:Promise解决后,会得到一个
Response对象。这个对象包含了响应的所有元信息(状态码、头信息等),但不直接包含响应体。 - 解析响应体:你需要调用
Response对象上的方法来解析响应体,例如.json()、.text()、.blob()等。这些方法同样返回一个Promise,因为读取响应体本身也是一个异步操作。 - 获取最终数据:解析响应体的Promise解决后,你才能拿到最终的数据(如JSON对象、文本字符串等)。
这里有一个至关重要的细节:fetch只在网络故障或请求被阻止时才会拒绝(reject)返回的Promise。对于HTTP错误状态,如404或500,fetch的Promise依然会正常解决(resolve)。你需要在.then()中通过检查Response.ok或Response.status属性来判断请求是否真正成功。这是fetch与许多旧式库(如jQuery.ajax,它会在HTTP错误时触发错误回调)一个关键的行为差异,也是新手最容易忽略的“坑”。
3. 发起你的第一个Fetch请求:GET与POST
3.1 最基本的GET请求
让我们从一个最简单的例子开始,获取一个公开的API数据。
// 示例:从JSONPlaceholder获取一篇帖子 fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => { // 第一步:检查响应是否成功(状态码在200-299之间) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 第二步:解析JSON格式的响应体 return response.json(); }) .then(data => { // 第三步:处理获取到的数据 console.log('Post title:', data.title); }) .catch(error => { // 第四步:捕获和处理错误(网络错误或上面抛出的错误) console.error('There was a problem with the fetch operation:', error); });逐行解析与注意事项:
fetch(‘https://...’):发起一个GET请求到指定URL。由于我们没有传递第二个参数(配置对象options),fetch默认使用GET方法。- 第一个
.then(response => { ... }):当服务器返回响应(无论状态码是什么)时,这个回调函数被调用,参数response就是Response对象。if (!response.ok) { throw new Error(...); }:这是必须的检查。response.ok是一个布尔值,当状态码在200-299范围内时为true。如果为false,我们手动抛出一个错误,这个错误会被最后的.catch()捕获。如果不做这个检查,即使服务器返回404,你的代码也会继续尝试解析响应体,很可能导致后续错误。return response.json();:我们假设服务器返回的是JSON格式的数据。.json()方法读取整个响应体,并将其解析为JavaScript对象。它返回一个新的Promise。
- 第二个
.then(data => { ... }):当response.json()的Promise解决后,我们拿到解析好的数据data,并在这里进行业务处理(如打印标题)。 .catch(error => { ... }):捕获整个Promise链中发生的任何错误。这包括:- 网络错误(如域名无法解析、连接被拒绝)。
- 我们在第一个
.then()中手动抛出的HTTP错误。 - 响应体解析错误(例如,服务器返回的不是合法JSON,但
.json()被调用了)。
实操心得:错误处理是门面很多初学者会忘记检查
response.ok。请务必养成习惯:每次使用fetch,第一个.then里先检查响应状态。一个健壮的错误处理机制,是生产环境代码和玩具代码的重要区别。
3.2 发送数据的POST请求
向服务器提交数据,例如创建新资源,我们通常使用POST、PUT或PATCH方法。这需要通过fetch的第二个参数——配置对象options来指定。
// 示例:向JSONPlaceholder提交一篇新帖子 const newPost = { title: 'My New Post', body: 'This is the content of my new post.', userId: 1, }; fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', // 指定请求方法 headers: { 'Content-Type': 'application/json', // 告诉服务器我们发送的是JSON // 可以添加其他头信息,如授权令牌 // 'Authorization': 'Bearer your_token_here' }, body: JSON.stringify(newPost), // 将JavaScript对象序列化为JSON字符串 }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log('Post created successfully:', data); // 服务器通常会返回创建的资源,包含新的ID console.log('New post ID:', data.id); }) .catch(error => { console.error('Error creating post:', error); });配置对象options详解:
method: 字符串,指定HTTP方法。常见的有‘GET’、‘POST’、‘PUT’、‘DELETE’、‘PATCH’。headers: 一个对象,用于设置请求头。这是与服务器通信的关键部分。‘Content-Type’:至关重要。它告诉服务器请求体的格式。发送JSON时,必须设置为‘application/json’。发送表单数据时,可能是‘application/x-www-form-urlencoded’或‘multipart/form-data’。- 其他常见头信息包括
‘Authorization’(用于身份验证)、‘Accept’(声明客户端希望接收的数据类型)等。
body: 请求体。对于GET或HEAD请求,通常为null或undefined。对于POST、PUT等需要发送数据的请求,这里放置要发送的数据。- 注意:
body的数据类型必须与headers中的‘Content-Type’匹配。发送JSON时,需要使用JSON.stringify()将对象转为字符串。 - 也可以发送
FormData、URLSearchParams、Blob等类型。
- 注意:
注意事项:Content-Type的匹配如果
body是JSON字符串,但Content-Type没设置或设置错误(如text/plain),服务器可能无法正确解析你的数据,导致请求失败。这是一个非常常见的问题源头。
4. 深入Response对象与数据解析
4.1 Response对象的常用属性和方法
Response对象是你与服务器响应交互的入口。除了.json(),它还有很多有用的属性和方法。
fetch('https://api.example.com/data') .then(response => { // 1. 状态信息 console.log('Status:', response.status); // 数字状态码,如 200 console.log('Status Text:', response.statusText); // 状态文本,如 "OK" console.log('OK?', response.ok); // 布尔值,状态码2xx为true // 2. 头信息 console.log('Headers:', response.headers); // 获取特定的头信息 const contentType = response.headers.get('content-type'); console.log('Content-Type:', contentType); // 3. URL和重定向信息 console.log('URL:', response.url); // 响应的最终URL(考虑重定向后) console.log('Redirected?', response.redirected); // 布尔值,请求是否被重定向 // 4. 根据Content-Type选择解析方法 if (contentType && contentType.includes('application/json')) { return response.json(); } else if (contentType && contentType.includes('text/html')) { return response.text(); } else { // 其他类型,如二进制文件 return response.blob(); } }) .then(parsedData => { // 处理解析后的数据 console.log('Parsed data:', parsedData); });解析方法选择指南:
.json(): 将响应体解析为JavaScript对象。仅当服务器返回合法JSON时使用,否则会抛出解析错误。.text(): 将响应体作为纯文本字符串返回。适用于HTML、XML、纯文本等。.blob(): 将响应体作为Blob(二进制大对象)返回。适用于图片、PDF、Excel等文件下载。.arrayBuffer(): 将响应体作为ArrayBuffer(原始二进制数据缓冲区)返回。用于更底层的二进制操作,如音频处理。.formData(): 将响应体解析为FormData对象。适用于服务器返回multipart/form-data格式的情况,较少见。
4.2 处理非JSON响应和错误
服务器并不总是返回JSON。你的代码需要足够健壮来处理不同的内容类型。
fetch('https://api.example.com/some-endpoint') .then(async (response) => { const contentType = response.headers.get('content-type'); if (!response.ok) { // 尝试获取错误信息,可能是JSON也可能是文本 let errorBody; if (contentType && contentType.includes('application/json')) { errorBody = await response.json(); throw new Error(`Request failed (${response.status}): ${JSON.stringify(errorBody)}`); } else { errorBody = await response.text(); throw new Error(`Request failed (${response.status}): ${errorBody}`); } } // 成功响应的处理 if (contentType && contentType.includes('application/json')) { return response.json(); } else { return response.text(); } }) .then(data => { console.log('Success:', data); }) .catch(error => { console.error('Fetch error:', error.message); });这个例子展示了更完善的错误处理:即使在HTTP错误时,也尝试根据Content-Type去读取响应体,以便获取服务器返回的更详细的错误信息。
5. 实战进阶:配置、超时与取消
5.1 完整的Fetch配置选项
fetch的第二个参数是一个强大的配置对象。除了method、headers、body,还有其他常用选项:
const controller = new AbortController(); const signal = controller.signal; // 设置一个超时,5秒后取消请求 setTimeout(() => controller.abort(), 5000); fetch('https://api.example.com/slow-data', { method: 'GET', headers: { 'Content-Type': 'application/json', }, // 请求模式:'cors', 'no-cors', 'same-origin'。默认'cors',用于跨域请求。 mode: 'cors', // 凭证(如Cookie)是否随请求发送:'omit', 'same-origin', 'include'。默认'same-origin'。 credentials: 'same-origin', // 缓存模式:'default', 'no-cache', 'reload', 'force-cache', 'only-if-cached'。 cache: 'default', // 重定向模式:'follow', 'error', 'manual'。 redirect: 'follow', // 引用策略:控制Referer头的发送。 referrerPolicy: 'no-referrer-when-downgrade', // 用于取消请求的AbortSignal对象 signal: signal, }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => { if (error.name === 'AbortError') { console.log('Fetch request was aborted due to timeout.'); } else { console.error('Fetch error:', error); } });关键配置解析:
mode: 处理跨域请求的关键。‘cors’(默认)允许跨域请求,但需要服务器正确配置CORS头。‘no-cors’用于请求不会“伤害”服务器的资源(如图片),但返回的Response是“不透明的”,你无法读取其内容。‘same-origin’只允许同源请求。credentials: 控制是否发送Cookie等凭证信息。如果需要携带Cookie进行身份验证,必须设置为‘include’。在mode: ‘cors’下,如果服务器允许凭证(Access-Control-Allow-Credentials: true),也必须设置此项。signal: 与AbortController配合,用于取消请求。这是处理请求超时或用户主动取消的现代标准方式。
5.2 实现请求超时与取消
原生fetch没有直接的超时参数,但我们可以利用AbortController和setTimeout轻松实现。
/** * 一个带超时功能的fetch包装函数 * @param {string} url - 请求URL * @param {object} options - fetch配置选项 * @param {number} timeoutMs - 超时时间(毫秒) * @returns {Promise} - 返回fetch的Promise,超时则拒绝 */ function fetchWithTimeout(url, options = {}, timeoutMs = 10000) { const controller = new AbortController(); const { signal } = controller; // 设置超时定时器 const timeoutId = setTimeout(() => { controller.abort(); console.warn(`Request to ${url} timed out after ${timeoutMs}ms`); }, timeoutMs); // 发起fetch请求,并传入signal const fetchPromise = fetch(url, { ...options, signal, // 将AbortSignal合并到options中 }).finally(() => { // 请求完成(无论成功或失败),清除定时器 clearTimeout(timeoutId); }); return fetchPromise; } // 使用示例 fetchWithTimeout('https://api.example.com/data', {}, 5000) // 5秒超时 .then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }) .then(data => console.log('Data received:', data)) .catch(error => { if (error.name === 'AbortError') { console.error('Request was aborted (likely due to timeout).'); } else { console.error('Request failed:', error); } });这个fetchWithTimeout工具函数非常实用,它封装了超时逻辑,使得业务代码更加简洁清晰。注意在.finally()中清理定时器,这是一个好习惯,避免内存泄漏。
6. 常见问题排查与性能优化技巧
6.1 典型错误与解决方案速查表
在实际开发中,你会遇到各种各样的问题。下面是一个快速排查指南:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
TypeError: Failed to fetch | 1. 网络连接问题(离线、URL错误、域名不存在)。 2. 请求被浏览器扩展(如广告拦截器)或CORS策略阻止。 3. 在 fetch调用前发生了JS错误。 | 1. 检查网络连接和URL拼写。 2. 打开浏览器开发者工具的“网络(Network)”面板,查看请求是否发出、状态是否为 (blocked:other)或CORS error。3. 检查控制台是否有其他先于fetch的错误。 |
SyntaxError: Unexpected token < in JSON at position 0 | 服务器返回的不是JSON(可能是HTML错误页面),但代码调用了.json()。 | 1. 检查response.ok,处理HTTP错误。2. 检查响应头 Content-Type,确认是application/json。3. 在调用 .json()前,先用.text()打印出响应体看看。 |
请求成功,但response.json()返回空或错误数据 | 1. 服务器返回的JSON格式不正确(如末尾有多余逗号)。 2. 字符编码问题。 | 1. 使用.text()获取原始字符串,用JSON.parse()尝试解析,捕获更具体的错误。2. 确保服务器使用UTF-8编码。 |
| Cookie或Session没有随请求发送 | fetch默认不发送凭证(credentials默认为‘same-origin’或‘omit’,取决于浏览器和模式)。 | 在fetch的options中显式设置credentials: ‘include’。同时,服务器CORS配置必须包含Access-Control-Allow-Credentials: true,且Access-Control-Allow-Origin不能为通配符*。 |
| 跨域(CORS)请求失败 | 服务器未正确配置CORS响应头。 | 1. 后端开发人员需要配置:Access-Control-Allow-Origin(允许的源)、Access-Control-Allow-Methods(允许的方法)、Access-Control-Allow-Headers(允许的请求头)。2. 前端可尝试设置 mode: ‘no-cors’,但这样你将无法读取响应内容。 |
| 请求无法取消或超时无效 | 未正确使用AbortController,或signal未传递给fetch。 | 确保创建了AbortController实例,并将其signal属性传递给fetch的options。调用controller.abort()来触发取消。 |
6.2 性能优化与最佳实践
复用Headers对象:如果你需要多次发送相同头信息的请求,可以创建一个
Headers对象实例并复用,这比每次都传递一个普通对象字面量更高效。const myHeaders = new Headers(); myHeaders.append('Content-Type', 'application/json'); myHeaders.append('Authorization', 'Bearer token123'); fetch(url1, { method: 'GET', headers: myHeaders }); fetch(url2, { method: 'POST', headers: myHeaders, body: data });谨慎处理大响应体:对于非常大的JSON文件(如你提到的超过500MB),直接使用
response.json()可能会阻塞主线程并消耗大量内存。此时应考虑使用流式处理。response.body是一个ReadableStream。你可以使用response.body.getReader()创建一个读取器,分块(chunk)读取和处理数据,而不是一次性加载到内存中。这需要更复杂的代码,但对于大文件至关重要。
使用
async/await语法糖:它能让异步代码看起来更像同步代码,提高可读性。上面的例子用async/await重写: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); } catch (error) { console.error('Fetch failed:', error); } } fetchData();async/await是基于Promise的语法糖,错误处理用try...catch包裹,逻辑更清晰。考虑请求竞态问题:在SPA(单页应用)中,快速切换视图可能触发多个请求。如果后发请求先返回,可能会覆盖先发请求的结果,导致状态错乱。解决方案是使用
AbortController在发起新请求时取消旧的相同请求,或者使用标志位(如请求ID)来忽略过时的响应。
掌握了这些基础,你已经能够应对日常开发中90%以上的网络请求场景。fetch的威力远不止于此,在后续的篇章中,我们将探讨更高级的主题,如请求/响应拦截、流式处理、与Service Worker的结合等,让你真正成为网络请求的驾驭者。记住,扎实的基础是通往精通的必经之路,多动手实践,多思考背后的原理,你写出的代码会越来越稳健。