Vue + AbortController 请求取消弹窗 hook 封装

背景

实际业务开发场景中,往往存在有些大数据请求的需求,一旦请求发起加载遮罩后用户就无法操作了,直接尬住,所以提供一个支持取消查询的功能还是很有必要的,为了在全业务接口都能使用封装一个hook。

✋为什么要用 AbortController?

AbortController 是浏览器提供的原生 API,用于中止 Web 请求(如 Fetch)。你可以通过调用 abort() 来通知一个绑定了该信号(signal)的请求停止执行。

简单来说,它的用法是这样的:

ts 复制代码
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 的封装库时。

🧠 思考:hook设计需解决的问题?

  • 启动查询时,展示一个 loading 动画。

  • 2 秒后仍未返回结果,弹出一个"取消查询"的提示框。

  • 如果用户点击"取消",则主动中止请求。

  • 请求成功或被取消后,清除提示框和 loading。

这就是我们这个 Hook 要完成的全部职责。

🏃‍➡ 实现步骤

第一步:定义中止信号 signal

我们在 Hook 中需要一个 ref 来存储当前的 AbortSignal,方便传给请求调用者。

ts 复制代码
const signal = ref<AbortSignal>({} as AbortSignal);

同时,我们在每次调用前重新创建一个 AbortController,保证每次请求都能独立控制。

第二步:启动 loading 和延迟弹窗

当用户点击"查询"按钮后,我们要立即显示一个 ElLoading 动画,然后 延迟 2 秒 后再弹出取消窗口。

为什么要延迟?

很多请求会在 2 秒内返回,没必要给用户太多打扰。我们只在"慢"的时候,才提醒用户可以取消。

ts 复制代码
loading = ElLoading.service({
  lock: true,
  text: '',
  background: 'rgba(0,0,0,0.2)',
});

 timer = setTimeout(() => {
      ElMessageBox.confirm(
        '<div class="flex flex-col gap-3 items-center"><div class="w-10 h-10 border-4 border-t-blue-500 border-gray-300 rounded-full animate-spin"></div><p>查询中...</p></div>',
        '提示',
        {
          dangerouslyUseHTMLString: true,
          customClass: 'custom-style',
          showClose: false,
          showCancelButton: false,
          confirmButtonText: '取消查询',
          closeOnClickModal: false,
          closeOnPressEscape: false,
        },
      ).then(() => {
        // 中止请求
        controller.abort();
        // 中止 后端真实请求查询
        stopTrueRequest().then(() => {
          ElMessage.success('已取消查询!');
        });
      });
    }, 2000);

这个 MessageBox 是关键,它展示了一个动画+提示文字,并提供"取消查询"的按钮。当点击时,我们会执行:

ts 复制代码
controller.abort();

中止请求,取消回调里可以调用真实取消查询的接口。

第三步:清理 timer 和 loading

无论请求成功还是被取消,我们都要记得清理 timer 和 loading:

ts 复制代码
const cancelPendingAlert = () => {
  loading?.close();
  if (timer) {
    clearTimeout(timer);
    timer = null;
  }
};
// 卸载时也要清理
onUnmounted(() => {
  if (timer) clearTimeout(timer);
  if (loading) loading.close();
});

最终 Hook 导出结构

ts 复制代码
return {
  loadCancelAlert,
  cancelPendingAlert,
  signal,
};

🚀 如何在页面中调用?

我们在业务组件中使用这个 Hook 时,可以这样写:

ts 复制代码
const { loadCancelAlert, cancelPendingAlert, signal } = useCancelRequest();

const testCancel = () => {
  loadCancelAlert(); // 显示 loading & 准备弹窗

  testCancelApi('', signal.value)
    .then(() => {
      cancelPendingAlert();
    })
    .finally(() => {
      cancelPendingAlert(); // 兜底关闭
      ElMessageBox.close(); // 主动关闭提示框
    });
};

注意这里的 testCancelApi 是你封装的接口请求函数,它需要支持接收 signal:

ts 复制代码
export function testCancelApi<T>(data: string, signal: AbortSignal) {
  return request<T>({
    url: '/api/v1/test',
    method: 'POST',
    data,
    signal, // ✨ 添加 signal 支持中断
  });
}

最终效果

界面

实际请求

hook 完整代码

javascript 复制代码
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(
        '<div class="flex flex-col gap-3 items-center"><div class="w-10 h-10 border-4 border-t-blue-500 border-gray-300 rounded-full animate-spin"></div><p>查询中...</p></div>',
        '提示',
        {
          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,
  };
}

完结撒花🎉🎉🎉

欢迎点赞+收藏+关注😀

相关推荐
俊昭喜喜里4 小时前
理解全局跨域CORS配置
前端
香芋芋圆4 小时前
iD-editor 详解|OSM官方在线编辑器,基于D3.js的轻量地图编辑器
开发语言·javascript·编辑器
晗夜未央6 小时前
任务队列反复失败,先分清重试、阻塞和结果待核对
开发语言·javascript
锋行天下7 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
jktantan7 小时前
每个节点 41 字节:把一棵百万节点树拆进 14 个 typed array
前端·javascript·vue.js·算法·typescript
计算机魔术师7 小时前
Anthropic官方插件被「寄生」,背后是同一类攻击又来了
前端
我命由我123458 小时前
访客系统形式(有用户概念、无用户概念)
java·开发语言·前端·javascript·python·产品运营·产品经理
key_3_feng8 小时前
评估与对齐:Agent 自我进化的护栏
前端·数据库·agent
程序员大阳8 小时前
副队长JavaScript教程(65)-详解JSON:轻量级数据交换格式
javascript·json·序列化·反序列化·stringify·parse
LaughingZhu9 小时前
Product Hunt 热榜 2026-10-30:Empacer、Scape、Floot in ChatGPT
java·前端·数据库·人工智能·chatgpt·notion