abortController、signal配合axios实现一个可以中断请求的操作

4729次阅读 363人点赞 作者: WuBin 发布时间: 2025-11-05 09:24:15
扫码到手机查看

问题说明

有如下情况,当用户每次进入***/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/catchcatch块),无法通过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

我们直接看vue组合API中的组件实现逻辑:
/* 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 内置的机制生效了,具体如下:

  1. AbortController 与 AbortSignal 的作用AbortController是浏览器原生 API,通过abortController.signal生成一个信号对象,可传递给 fetch/axios 等请求工具。当调用abortController.abort()时,该信号会被触发,请求会立即中断。

  2. 在 http.js 中的隐含处理

    虽然 http.js 没有显式写处理signal的代码,但 axios 内部会识别并处理配置中的signal参数,

    axios 会监听signal的状态,当信号被中止(abort()调用)时,会立即终止请求并抛出AbortError。也就是http.js中监听的:ERR_CANCELED

  3. 在当前业务中

    组件卸载时会调用abortController.abort()

onBeforeUnmount(() => {
  if (abortController) {
    abortController.abort(); // 中断请求
    abortController = null;
  }
});

此时,通过signal绑定的请求会被立即终止,避免组件已经卸载后,请求仍在继续(可能导致内存泄漏或无用的错误提示)。

总结一下就是这么封装好之后,组件内部需要中断的时候,发送abortController.abort(); 只要signal传给了axios,那么axios就会自动处理中断的请求。

点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:axios,abortController,signal
推荐阅读
  • python基础-操作列表和迭代器

    python基础笔记-操作列表和迭代器的相关方法

    6708次阅读 164人点赞 发布时间: 2024-06-13 13:26:27 立即查看
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    12195次阅读 825人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    15412次阅读 644人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    16294次阅读 1300人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    18705次阅读 1501人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    16879次阅读 1365人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    32703次阅读 2567人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    28520次阅读 2196人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录