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,
  };
}

完结撒花🎉🎉🎉

欢迎点赞+收藏+关注😀

相关推荐
秉承初心几秒前
webpack和vite对比解析(AI)
前端·webpack·node.js
团酱2 分钟前
sass-loader与webpack版本冲突解决方案
前端·vue.js·webpack·sass
我是来人间凑数的7 分钟前
electron 配置特定文件右键打开
前端·javascript·electron
安心不心安33 分钟前
React封装框架dvajs(状态管理+异步操作+数据订阅等)
前端·react.js·前端框架
未来之窗软件服务1 小时前
js调用微信支付 第二步 获取access_token ——仙盟创梦IDE
开发语言·javascript·微信·微信支付·仙盟创梦ide·东方仙盟
可可格子衫1 小时前
keep-alive缓存文章列表案例完整代码(Vue2)
vue.js·缓存
洛小豆1 小时前
为什么可以通过域名访问接口,但不能通过IP地址访问接口?
前端·javascript·vue.js
武昌库里写JAVA1 小时前
VUE vuex深入浅出
vue.js·spring boot·毕业设计·layui·课程设计
代码老y1 小时前
Spring Boot + MyBatis + Vue:从零到一构建全栈应用
vue.js·spring boot·mybatis
要加油哦~2 小时前
vue | rollup 打包 | 配置 rollup.config.js 文件,更改 rollup的行为
前端