实际业务开发场景中,往往存在有些大数据请求的需求,一旦请求发起加载遮罩后用户就无法操作了,直接尬住,所以提供一个支持取消查询的功能还是很有必要的,为了在全业务接口都能使用封装一个hook。
AbortController
是浏览器提供的原生 API,用于中止 Web 请求(如 Fetch)。你可以通过调用 abort()
来通知一个绑定了该信号(signal
)的请求停止执行。
简单来说,它的用法是这样的:
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/slow-request', { signal }).catch(err => {
if (err.name === 'AbortError') {
console.log('请求被中止了');
}
});
setTimeout(() => {
controller.abort(); // 中止请求
}, 2000);
这在 Vue 中也完全适用,尤其是你使用 Axios、Fetch 或其他支持 AbortSignal 的封装库时。
启动查询时,展示一个 loading 动画。
2 秒后仍未返回结果,弹出一个“取消查询”的提示框。
如果用户点击“取消”,则主动中止请求。
请求成功或被取消后,清除提示框和 loading。
这就是我们这个 Hook 要完成的全部职责。
我们在 Hook 中需要一个 ref 来存储当前的 AbortSignal,方便传给请求调用者。
const signal = ref<AbortSignal>({} as AbortSignal);
同时,我们在每次调用前重新创建一个 AbortController,保证每次请求都能独立控制。
当用户点击“查询”按钮后,我们要立即显示一个 ElLoading 动画,然后 延迟 2 秒 后再弹出取消窗口。
很多请求会在 2 秒内返回,没必要给用户太多打扰。我们只在“慢”的时候,才提醒用户可以取消。
loading = ElLoading.service({
lock: true,
text: '',
background: 'rgba(0,0,0,0.2)',
});
timer = setTimeout(() => {
ElMessageBox.confirm(
'查询中...
',
'提示',
{
dangerouslyUseHTMLString: true,
customClass: 'custom-style',
showClose: false,
showCancelButton: false,
confirmButtonText: '取消查询',
closeOnClickModal: false,
closeOnPressEscape: false,
},
).then(() => {
// 中止请求
controller.abort();
// 中止 后端真实请求查询
stopTrueRequest().then(() => {
ElMessage.success('已取消查询!');
});
});
}, 2000);
这个 MessageBox 是关键,它展示了一个动画+提示文字,并提供“取消查询”的按钮。当点击时,我们会执行:
controller.abort();
中止请求,取消回调里可以调用真实取消查询的接口。
无论请求成功还是被取消,我们都要记得清理 timer 和 loading:
const cancelPendingAlert = () => {
loading?.close();
if (timer) {
clearTimeout(timer);
timer = null;
}
};
// 卸载时也要清理
onUnmounted(() => {
if (timer) clearTimeout(timer);
if (loading) loading.close();
});
return {
loadCancelAlert,
cancelPendingAlert,
signal,
};
我们在业务组件中使用这个 Hook 时,可以这样写:
const { loadCancelAlert, cancelPendingAlert, signal } = useCancelRequest();
const testCancel = () => {
loadCancelAlert(); // 显示 loading & 准备弹窗
testCancelApi('', signal.value)
.then(() => {
cancelPendingAlert();
})
.finally(() => {
cancelPendingAlert(); // 兜底关闭
ElMessageBox.close(); // 主动关闭提示框
});
};
注意这里的 testCancelApi 是你封装的接口请求函数,它需要支持接收 signal:
export function testCancelApi<T>(data: string, signal: AbortSignal) {
return request<T>({
url: '/api/v1/test',
method: 'POST',
data,
signal, // ✨ 添加 signal 支持中断
});
}
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import { onUnmounted, ref } from 'vue';
export default function useCancelRequest() {
const signal = ref<AbortSignal>({} as AbortSignal); // 终止标识
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器延迟弹窗加载
let loading: LoadingInstance;
/** 初始化取消请求弹窗 */
const loadCancelAlert = () => {
const controller = new AbortController(); // 请求终止器
signal.value = controller.signal;
loading = ElLoading.service({
lock: true,
text: '',
background: 'rgab(0,0,0,0.2)',
});
timer = setTimeout(() => {
ElMessageBox.confirm(
'查询中...
',
'提示',
{
dangerouslyUseHTMLString: true,
customClass: 'custom-style',
showClose: false,
showCancelButton: false,
confirmButtonText: '取消查询',
closeOnClickModal: false,
closeOnPressEscape: false,
},
).then(() => {
// 中止请求
controller.abort();
// 中止 后端真实请求查询
stopTrueRequest().then(() => {
ElMessage.success('已取消查询!');
});
});
}, 2000);
};
// 请求完成时调用,取消加载取消请求弹窗
const cancelPendingAlert = () => {
loading?.close();
if (timer) {
clearTimeout(timer);
timer = null;
}
};
onUnmounted(() => {
if (timer) clearTimeout(timer);
if (loading) loading.close();
});
return {
loadCancelAlert,
cancelPendingAlert,
signal,
};
}
完结撒花
欢迎点赞+收藏+关注