abortController、signal配合axios实现一个可以中断请求的操作
问题说明
有如下情况,当用户每次进入***/my-cd这个路由的时候,会发送一个拉取列表的请求,但是由于网络环境不好,可能出现用户长时间等待的情况。这时候,当用户退出的时候,我需要让当前正在发送的请求立即中断(我是将整个请求所有的请求失败进行了统一的处理,失败的时候都会进行弹窗提示)所以这就会导致,用户进入路由->发送请求->请求长时间等待响应->用户退出当前路由前往其他路由->此时上一个响应完成,弹窗提示失败,非常影响体验。
为了解决这个问题,我考虑在用户退出路由的时候,可以中断当前路由的请求,避免资源浪费。
首先是我基于axios统一封装的底层请求,http.js:
import axios from 'axios';
import { CODE_OK, BASE_URL, ERR_CODE, TIME_OUT, SERVICE_NOT_NEED_LOADING } from "./config";
import { showAlert, loadingAlert } from "@/common/js/sweet-alert";
// 创建 axios 实例
const instance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
// 设置超时时间为30秒
timeout: TIME_OUT,
// withCredentials: true, // 如需跨域携带 cookie 可开启
});
// 添加请求拦截器:自动在 Header 中添加 Token
instance.interceptors.request.use(
(config) => {
// 判断接口是否在“不需要 loading”的列表中
const isNeedLoading = !SERVICE_NOT_NEED_LOADING.includes(config.url);
// 需要 loading 时,创建并存储(返回的是自定义对象,含 close 方法)
if (isNeedLoading) {
// // 可传参修改最小时间,如 loadingAlert(500) 弹窗等待几秒
config.loadingInstance = loadingAlert(500);
}
// 从 localStorage 中获取 Token
const token = '123456789';
if (token) {
// 将 Token 添加到请求头(格式通常为 Bearer + 空格 + Token,具体看后端要求)
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
// 处理请求错误
return Promise.reject(error);
}
);
// 添加响应拦截器:关闭加载弹窗并处理超时错误
instance.interceptors.response.use(
async (response) => {
// // 只有存在 loadingInstance 且有 close 方法时,才执行关闭
if (response.config.loadingInstance?.close) {
await response.config.loadingInstance.close();
}
return response;
},
async (error) => {
console.log(error);
// 关闭加载弹窗
if (error.config &&
error.config.loadingInstance?.close
) {
await error.config.loadingInstance.close();
}
// 处理超时错误
switch (error.code) {
case 'ECONNABORTED':
showAlert({
title: '服务器繁忙',
text: '请稍候刷新重试',
icon: 'error',
});
break;
// 主动中断请求
case 'ERR_CANCELED':
console.log('主动中断请求', error.code);
break;
default:
// 处理其他网络错误
showAlert({
title: '网络错误',
text: '无法连接到服务器,请检查网络设置后,刷新重试',
icon: 'error',
});
}
return Promise.reject(error);
}
);
/*
* GET 请求封装
* params 必须是一个对象
* config 可选配置(如 signal、headers 等axios配置)
* */
export function get(url, params, config = {}) {
if (!(typeof params === 'object')) {
params = { params };
}
// 将 params 和 config 合并到 axios 请求配置中(config 优先级更高,可覆盖 params)
const requestConfig = {
params,
...config // 合并 signal 等配置
};
return instance.get(url, requestConfig).then((res) => { // 传递合并后的配置
const serverData = res.data;
if (serverData.code === CODE_OK) {
return serverData.data;
} else {
errorHandle(serverData);
throw new Error(serverData.msg || '接口请求失败');
}
}).catch((e) => {
// 关键:过滤主动中断的 AbortError,避免误触发错误提示
if (e.name !== 'AbortError') {
console.log('请求错误:', e);
}
return null;
});
}
/*
* POST 请求封装(同 GET 逻辑,增加 config 参数)
* params 必须是一个对象
* config 可选配置(如 signal、headers 等axios配置)
* */
export function post(url, params, config = {}) { // 新增 config 参数
if (!(typeof params === 'object')) {
params = { params };
}
// POST 请求中,axios 第二个参数是 data,第三个是 config,需注意参数顺序
return instance.post(url, params, config).then((res) => { // 传递 config 到第三个参数
const serverData = res.data;
console.log(serverData)
if (serverData.code === CODE_OK) {
return serverData.data;
} else {
errorHandle(serverData);
throw new Error(serverData.msg || '接口请求失败');
}
}).catch((e) => {
// 同样过滤 AbortError
if (e.name !== 'AbortError') {
console.log('请求错误:', e);
}
return null;
});
}
function errorHandle(serverData) {
switch (true) {
case ERR_CODE.cdkey.includes(serverData.code):
showAlert({
text: serverData.msg,
icon: 'warning'
});
break;
default:
showAlert({
text: serverData.msg
});
console.log(serverData);
}
}
这里关于post和get,还有一点要说明,他们最后如果进入到catch,结果返回的是return null。
还有一种写法:
export function post(url, params, config = {}) { // 新增 config 参数
if (!(typeof params === 'object')) {
params = { params };
}
// POST 请求中,axios 第二个参数是 data,第三个是 config,需注意参数顺序
return instance.post(url, params, config).then((res) => { // 传递 config 到第三个参数
...
}).catch((e) => {
Promise.reject(null)
});
}这里绝对不要改为Promise.reject(null)
先明确两者的核心区别:
| 写法 | 本质 | 前端调用时的表现 |
|---|---|---|
return null | 成功态(resolved) | 会进入await的 “成功结果”,response直接等于null,用if(!response)判断失败即可。 |
Promise.reject(null) | 失败态(rejected) | 会直接触发catch(或try/catch的catch块),无法通过response拿到null。 |
如果失败返回的是null,那么前端调用的时候,只需要:
res = await post(xxx)if(!res) {.. 失败逻辑..}而如果在catch中使用Promise.reject(null),那么在前端的await就需要在包一层try-catch
try {res = await post(xxx)} catch (error) { 你必须额外加 try-catch 才能捕获这个 reject}但是这么做的前提就是:必须明确 “后端正常响应只会返回数组 / 对象,不会返回
null”,
config.js
import { DEBUG } from "@/common/js/const";
export const CODE_OK = 0;
// 需要沟通错误码
export const ERR_CODE = {
cdkey: [1001, 1002]
};
// 请求超时时间 30秒
export const TIME_OUT = 10 * 1000;
// 请求的地址
export const BASE_URL = DEBUG ? '/api/' : `server/`;
export const SERVICE = {
user: 'user',
bind_cdkey: '****',
my_cdkey: '***'
};
// 不需要弹窗等待的接口 自定义等待处理逻辑
export const SERVICE_NOT_NEED_LOADING = [
SERVICE.user,
SERVICE.my_cdkey
];request.js 将底层请求方法进行语义化封装
export async function getMyCdkeys(userId, { signal } = {}) {
return get(SERVICE.my_cdkey,
{
user_id: userId,
delay: 2 * 1000
},
{ signal }
);
}组件中的abortController
/* eslint-disable */
import {ref, computed, nextTick, onBeforeUnmount, onMounted} from 'vue';
import {useStore} from 'vuex';
import {getMyCdkeys} from "@/service/request";
import { useRoute, useRouter } from 'vue-router';
export default function () {
const store = useStore();
// 路由信息对象(命名改为 route 更规范)
const route = useRoute();
// 路由实例(用于导航操作)
const router = useRouter();
// 创建请求中断控制器
let abortController = null; // 用于跟踪当前请求
const isWaitingRequestCdkey = ref(true);
const cdkeyList = computed(() => store.getters.cdkeyList);
const userID = 123;
// 用 AbortController 包装请求
const fetchCdkeys = async () => {
if (abortController) {
return;
}
// 每次请求前创建新的控制器(避免复用已中断的实例)
abortController = new AbortController();
const resList = await getMyCdkeys(userID.value, {
// 绑定中断信号
signal: abortController.signal
});
// 请求失败
if (resList === null) {
// 若还留在当前组件 那么返回上一级
if (route.name === '****-my-cd') {
router.go(-1);
}
return;
}
isWaitingRequest.value = false;
store.commit('setCdkeyList', resList);
};
onMounted(() => {
// 确保组件完全挂载后再发起请求
fetchCdkeys();
});
onBeforeUnmount(() => {
if (abortController) {
abortController.abort();
// 重置控制器
abortController = null;
isWaitingRequest.value = true;
console.log('组件卸载,中断CDKey请求');
}
});
return {
****
}
}
signal: abortController.signal的作用是将请求与一个AbortSignal绑定,用于在需要时主动中断未完成的请求(例如组件卸载、路由跳转时),避免无效请求继续占用资源或导致不必要的错误。
虽然在 http.js 中没有显式处理signal的逻辑,但它通过 axios 内置的机制生效了,具体如下:
AbortController 与 AbortSignal 的作用
AbortController是浏览器原生 API,通过abortController.signal生成一个信号对象,可传递给 fetch/axios 等请求工具。当调用abortController.abort()时,该信号会被触发,请求会立即中断。在 http.js 中的隐含处理
虽然 http.js 没有显式写处理
signal的代码,但 axios 内部会识别并处理配置中的signal参数,axios 会监听
signal的状态,当信号被中止(abort()调用)时,会立即终止请求并抛出AbortError。也就是http.js中监听的:ERR_CANCELED在当前业务中
组件卸载时会调用
abortController.abort():
onBeforeUnmount(() => {
if (abortController) {
abortController.abort(); // 中断请求
abortController = null;
}
});此时,通过signal绑定的请求会被立即终止,避免组件已经卸载后,请求仍在继续(可能导致内存泄漏或无用的错误提示)。
总结一下就是这么封装好之后,组件内部需要中断的时候,发送abortController.abort(); 只要signal传给了axios,那么axios就会自动处理中断的请求。
目录