Fetch API 高阶玩法:拦截、变形与超时大作战
Fetch API,这玩意儿,前端工程师天天打交道,就像老朋友一样。你可能已经用它发送过无数个GET、POST请求,熟练得像呼吸一样自然。但老朋友也得常联系,不然时间长了,难免会有些生疏。今天咱们就来聊聊 Fetch API 的一些“高阶玩法”,让你对这位老朋友有更深的了解,关键时刻能派上大用场。
咱们今天的主题是:请求拦截、响应处理与超时控制。听起来有点学术,但其实一点都不难。想象一下,你是一个餐厅的服务员,Fetch API 就是你,餐厅厨房是后端服务器,顾客就是你的前端代码。
- 请求拦截:就像你在顾客点完菜后,先检查一下厨房的食材够不够,或者顾客有没有特殊要求,然后再把菜单交给厨师。
- 响应处理:厨师做完菜,你端上来之前,先看看菜品卖相如何,有没有少放盐,然后再呈现给顾客。
- 超时控制:顾客等太久会不高兴,所以你要设置一个上菜时间,超过时间就给顾客打个折,或者推荐一道更快的手抓饼。
这样是不是一下子就明白了?好,接下来咱们就深入探讨一下这些“高阶玩法”。
拦截请求:当个称职的“拦截器”
在现实生活中,拦截器无处不在。比如高速公路上的收费站,机场的安检等等。在前端开发中,我们也可以使用拦截器来处理请求,比如:
- 统一添加请求头:比如 Authorization (token),Content-Type等,省去每个请求都手动添加的麻烦。
- 请求参数的修改:在请求发送之前,对参数进行统一处理,比如加密、格式化等等。
- 请求的权限控制:根据用户的权限,决定是否允许发送请求。
- 请求的日志记录:记录每个请求的信息,方便调试和分析。
那怎么实现呢? Fetch API 本身并没有提供原生的拦截器功能。但是,我们可以通过一些技巧来实现类似的效果,核心思想就是“包装 Fetch 函数”。
// 原始的 fetch 函数
const originalFetch = window.fetch;
// 包装后的 fetch 函数
window.fetch = async (...args) => {
// 1. 请求拦截:在请求发送前进行处理
const request = args[0]; // 请求的 URL 或 Request 对象
const options = args[1] || {}; // 请求的配置选项
// 统一添加 Authorization 请求头
const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage 中
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
// 记录请求日志
console.log('Request:', request, options);
// 2. 发送请求
try {
const response = await originalFetch(...args);
// 3. 响应处理:在响应返回后进行处理 (后面会详细讲)
return response;
} catch (error) {
// 统一处理错误
console.error('Request Error:', error);
throw error; // 别忘了抛出错误,不然调用方就不知道出错了
}
};
这段代码做了什么呢?
- 保存原始的 Fetch 函数:
const originalFetch = window.fetch;
这句话把浏览器原生的fetch
函数存了起来,防止被覆盖后找不着北。 - 重写 Fetch 函数:
window.fetch = async (...args) => { ... };
这行代码把浏览器的fetch
函数重新定义了,以后你代码里用的fetch
都是这个被我们“动过手脚”的函数了。 - 请求拦截: 在新的
fetch
函数里,我们拿到请求的参数(URL 和配置),然后就可以为所欲为了。比如,我们这里统一添加了Authorization
请求头,从localStorage
里取出token,加到请求头里。以后你再用fetch
发请求,就不用每次都手动加token了,省事儿! - 发送请求:
const response = await originalFetch(...args);
这行代码才是真正发送请求的地方。我们调用之前保存的原始fetch
函数,把参数传进去,然后等待服务器返回响应。 - 错误处理:
try...catch
语句包裹了整个请求过程,如果请求过程中发生了错误,我们可以在catch
里统一处理。比如,记录错误日志,或者显示一个友好的错误提示。
现在,你每次使用 fetch
发送请求,都会先经过这个“拦截器”,统一添加请求头,记录日志,处理错误。是不是感觉代码一下子变得高大上了?
举个栗子:
假设你有一个用户认证系统,用户登录后会得到一个 token,你需要把这个 token 放在每个请求的 Authorization
头里。有了这个拦截器,你只需要在用户登录成功后,把 token 存到 localStorage
里,然后就不用管了。拦截器会自动帮你把 token 加到每个请求头里,简直不要太方便!
// 用户登录成功后
localStorage.setItem('token', 'your_awesome_token');
// 发送请求,不需要手动添加 Authorization 头
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data));
响应处理:给返回值化个妆
后端返回的数据,有时候并不是我们想要的格式。比如,有的后端喜欢用 code
和 message
来表示状态码和错误信息,有的后端喜欢直接返回数据。为了统一处理这些差异,我们可以在响应返回后,对数据进行一些处理。
// 还是上面的拦截器代码,我们只关注响应处理部分
window.fetch = async (...args) => {
// ... (省略请求拦截部分)
try {
const response = await originalFetch(...args);
// 2. 响应处理:在响应返回后进行处理
if (!response.ok) {
// 处理 HTTP 错误
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
// 如果是 JSON 数据,先解析成 JSON 对象
const data = await response.json();
// 统一处理状态码
if (data.code !== 200) {
// 假设 code 200 表示成功
throw new Error(data.message || 'Request failed');
}
// 返回真正的数据
return data.data; // 假设 data.data 才是真正的数据
} else {
// 如果不是 JSON 数据,直接返回 response 对象
return response;
}
} catch (error) {
// ... (省略错误处理部分)
}
};
这段代码做了什么呢?
- 检查 HTTP 状态码:
if (!response.ok) { ... }
这行代码检查服务器返回的 HTTP 状态码是否正常。如果状态码不是 200-299 之间,就表示请求出错了,我们直接抛出一个错误。 - 判断响应类型:
const contentType = response.headers.get('content-type');
这行代码获取响应的Content-Type
头,判断响应的数据类型。 - 处理 JSON 数据: 如果响应是 JSON 数据,我们就先用
response.json()
方法把数据解析成 JSON 对象。然后,我们可以根据后端返回的数据格式,对数据进行统一处理。比如,如果后端用code
和message
来表示状态码和错误信息,我们可以检查code
是否为 200,如果不是,就抛出一个错误。最后,我们返回真正的数据(假设后端把数据放在data.data
字段里)。 - 处理非 JSON 数据: 如果响应不是 JSON 数据,我们就直接返回
response
对象,让调用方自己处理。
有了这个响应处理,你就可以统一处理后端返回的数据格式,不用在每个请求里都写重复的代码。
举个栗子:
假设你的后端返回的数据格式如下:
{
"code": 200,
"message": "success",
"data": {
"name": "John Doe",
"age": 30
}
}
有了上面的响应处理,你只需要这样写代码:
fetch('/api/user')
.then(data => {
// data 就是 { name: "John Doe", age: 30 },不再需要手动解析 code 和 message
console.log(data.name); // John Doe
})
.catch(error => {
console.error(error); // 如果 code 不是 200,会抛出错误
});
是不是感觉清爽了很多?
超时控制:不能让用户等的花儿都谢了
网络请求,最怕的就是“卡壳”。用户点了提交按钮,结果页面一直转圈圈,等了半天也没反应,体验简直差到极点。所以,我们需要对请求设置一个超时时间,如果超过时间还没返回,就自动取消请求,给用户一个友好的提示。
Fetch API 本身并没有提供原生的超时控制功能,但是我们可以使用 AbortController
来实现。
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const signal = controller.signal;
// 设置定时器,超时后取消请求
const timeoutId = setTimeout(() => {
controller.abort(); // 取消请求
}, timeout);
try {
const response = await fetch(url, { ...options, signal });
clearTimeout(timeoutId); // 清除定时器,防止请求已经成功返回,定时器还在执行
return response;
} catch (error) {
clearTimeout(timeoutId); // 也要清除定时器,防止内存泄漏
if (error.name === 'AbortError') {
// 请求超时
throw new Error('Request timed out');
}
throw error; // 其他错误
}
}
这段代码做了什么呢?
- 创建 AbortController:
const controller = new AbortController();
AbortController
是一个用于取消 Web 请求的 API。 - 获取 signal:
const signal = controller.signal;
signal
对象用于与fetch
请求关联,当调用controller.abort()
时,signal
会发出一个取消信号,fetch
请求就会被取消。 - 设置定时器:
setTimeout(() => { ... }, timeout);
我们使用setTimeout
函数设置一个定时器,当超过timeout
时间后,就调用controller.abort()
方法取消请求。 - 发送请求:
const response = await fetch(url, { ...options, signal });
我们把signal
对象传递给fetch
函数,这样fetch
请求就和AbortController
关联起来了。 - 处理错误: 如果请求被取消,
fetch
函数会抛出一个AbortError
错误。我们可以在catch
语句中捕获这个错误,并进行相应的处理。 - 清除定时器:
clearTimeout(timeoutId);
非常重要!当请求成功返回或者发生其他错误时,我们需要清除定时器,防止定时器还在执行,导致内存泄漏。
举个栗子:
fetchWithTimeout('/api/data', {}, 3000) // 设置超时时间为 3 秒
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.message === 'Request timed out') {
// 处理超时错误
console.error('请求超时,请稍后再试');
} else {
// 处理其他错误
console.error(error);
}
});
这样,如果请求超过 3 秒还没有返回,就会自动取消请求,并显示“请求超时,请稍后再试”的提示。用户的体验是不是好多了?
总结
今天我们一起学习了 Fetch API 的三个高阶玩法:请求拦截、响应处理和超时控制。
- 请求拦截 就像一个门卫,可以统一处理请求头、参数等等。
- 响应处理 就像一个化妆师,可以统一处理后端返回的数据格式。
- 超时控制 就像一个闹钟,可以防止用户等太久。
掌握了这些技巧,你就可以更加灵活地使用 Fetch API,写出更加健壮、易维护的代码。当然,这些只是 Fetch API 的冰山一角,还有很多其他的技巧等待你去探索。希望这篇文章能给你带来一些启发,让你在前端开发的道路上越走越远!