vue3+ts Axios封装—重复请求拦截

发布时间 2023-09-04 14:14:32作者: 虚乄

创建好vue3项目

1.安装Axios与Element Plus

Axios安装

npm install axios

Element Plus 安装

官网入口:https://element-plus.gitee.io/zh-CN/

npm install element-plus --save

Element 主要用到信息提示 与 全屏加载动画

2.在src 目录下创建 api 文件夹和 utils 文件夹

api 文件夹下 封装 Axios封装 与 请求配置

utils 文件夹下 operate.ts 配置接口地址 与其他全局ts

 

3.Axios封装

旧版本地址:https://www.cnblogs.com/lovejielive/p/16363587.html

新版本:主要增加动态控制是否显示加载动画。

是否需要判断重复请求。

优化请求接口配置参数写法。

扩展AxiosRequestConfig 增加自定义参数

declare module 'axios' {
    //请求自定义参数
    interface AxiosRequestConfig {
        // 是否显示加载框
        ifLoading?: boolean
        // 是否允许重复请求
        repeatRequest?: boolean
        // 登录 token
        isToken?: any;
    }
}

3.1重复请求判断

通过配置repeatRequest是否允许重复请求,来开启判断。主要在api.ts中配置。

每一次请求创建一个key,判断是否存在,如存在执行.abort()取消当前请求,

不存在pendingMap中新增一个key。

通过AbortController来进行手动取消。

主要代码

//格式化请求链接
function getRequestKey(config: AxiosRequestConfig) {
    const { url, method, data, params } = config,
        //字符串化参数
        dataStr = JSON.stringify(data) || '',
        paramsStr = JSON.stringify(params) || '',
        //记得这里一定要处理 每次请求都掉会变化的参数(比如每个请求都携带了时间戳),否则二个请求的key不一样
        key = [method, url, dataStr, paramsStr].join("&");
    return key;
}

//创建存储 key 的 集合
const pendingMap = new Map()

//是否重复请求key
function setPendingMap(config: AxiosRequestConfig) {
    //手动取消
    const controller = new AbortController()
    config.signal = controller.signal
    const key = getRequestKey(config)
    //判断是否存在key 存在取消请求 不存在添加
    if (pendingMap.has(key)) {
        // abort取消请求
        pendingMap.get(key).abort()
        //删除key
        pendingMap.delete(key)
    } else {
        pendingMap.set(key, controller)
    }
}

在接口消息提示时,通过 axios.isCancel(error) 过滤掉已取消的请求,

//拦截掉重复请求的错误,中断promise执行
if (axios.isCancel(error)) return []

3.2 axios完整代码

api文件夹下 创建 request-wrapper.ts Axios封装
/*
 * @description: 请求封装
 * @Author: Jay
 * @Date: 2023-04-11 13:24:41
 * @LastEditors: Jay
 * @LastEditTime: 2023-09-04 11:49:10
 */

// 导入axios
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// 使用element-ui ElMessage做消息提醒  ElLoading加载
import { ElMessage, ElLoading } from "element-plus";
//请求头
import operate from "@/utils/operate"

//加载配置
let loadingInstance: { close: () => void };
let requestNum = 0;
//加载动画
const addLoading = () => {
    // 防止重复弹出
    requestNum++;
    if (requestNum === 1) {
        loadingInstance = ElLoading.service({ fullscreen: true });
    }
}
// 关闭 加载动画
const cancelLoading = () => {
    requestNum--;
    if (requestNum === 0) loadingInstance?.close();
}

//格式化请求链接
function getRequestKey(config: AxiosRequestConfig) {
    const { url, method, data, params } = config,
        //字符串化参数
        dataStr = JSON.stringify(data) || '',
        paramsStr = JSON.stringify(params) || '',
        //记得这里一定要处理 每次请求都掉会变化的参数(比如每个请求都携带了时间戳),否则二个请求的key不一样
        key = [method, url, dataStr, paramsStr].join("&");
    return key;
}

//创建存储 key 的 集合
const pendingMap = new Map()

//是否重复请求key
function setPendingMap(config: AxiosRequestConfig) {
    //手动取消
    const controller = new AbortController()
    config.signal = controller.signal
    const key = getRequestKey(config)
    //判断是否存在key 存在取消请求 不存在添加
    if (pendingMap.has(key)) {
        // abort取消请求
        pendingMap.get(key).abort()
        //删除key
        pendingMap.delete(key)
    } else {
        pendingMap.set(key, controller)
    }
}

//增加新的请求参数类型
declare module 'axios' {
    //请求自定义参数
    interface AxiosRequestConfig {
        // 是否显示加载框
        ifLoading?: boolean
        // 是否允许重复请求
        repeatRequest?: boolean
        // 登录 token
        isToken?: any;
    }

    // 解決 类型“AxiosResponse<any, any>”上不存在属性“code”
    // interface AxiosResponse<T = any> {
    //     // 请求 data 里的一级参数
    //     code: number;
    //     time: string;
    //     msg: string;
    //     data: T;
    // }
}

//创建axios的一个实例
const axiosInstance: AxiosInstance = axios.create({
    //接口统一域名
    baseURL: operate.baseUrl(),
    //设置超时
    timeout: 1000 * 30,
    //跨域携带cookie
    withCredentials: true,
})

// 添加请求拦截器
axiosInstance.interceptors.request.use(
    (config) => {
        //加载动画
        if (config?.ifLoading) addLoading();
        //是否判断重复请求
        if (!config.repeatRequest) {
            setPendingMap(config)
        }

        //判断是否有token 根据自己的需求判断
        const token = config.isToken
        console.log("判断是否有token", token)
        if (token != undefined) {
            //如果要求携带在参数中
            config.params = Object.assign({}, config.params, token)
            // 如果要求携带在请求头中
            // config.headers = Object.assign({}, config.headers, operate.uploadParameters())
        }
        return config
    },
    (error: AxiosError) => {
        return Promise.reject(error)
    }
)

// 添加响应拦截器
axiosInstance.interceptors.response.use((response: AxiosResponse) => {
    const config = response.config
    // 关闭加载 动画
    if (config?.ifLoading) cancelLoading();
    //是否登录过期
    if (response.data.code == 400 || response.data.code == 401) {
        ElMessage.error("登录过期,请重新登录")
        // //清除登录缓存
        // store.commit("LOGOUT")
        // //返回首页
        // setTimeout(() => {
        //     router.push("/");
        // }, 500);
        return
    }
    // 返回参数
    return response.data
})

// 错误处理
axiosInstance.interceptors.response.use(undefined, (error) => {
    const config = error.config
    // 关闭加载 动画
    if (config?.ifLoading) cancelLoading();

    //拦截掉重复请求的错误,中断promise执行
    if (axios.isCancel(error)) return []

    /***** 接收到异常响应的处理开始 *****/
    if (error && error.response) {
        // 1.公共错误处理
        // 2.根据响应码具体处理
        switch (error.response.status) {
            case 400:
                error.message = '错误请求'
                break;
            case 401:
                error.message = '未授权,请重新登录'
                break;
            case 403:
                error.message = '拒绝访问'
                break;
            case 404:
                error.message = '请求错误,未找到该资源'
                // window.location.href = "/NotFound"
                break;
            case 405:
                error.message = '请求方法未允许'
                break;
            case 408:
                error.message = '请求超时'
                break;
            case 500:
                error.message = '服务器端出错'
                break;
            case 501:
                error.message = '网络未实现'
                break;
            case 502:
                error.message = '网络错误'
                break;
            case 503:
                error.message = '服务不可用'
                break;
            case 504:
                error.message = '网络超时'
                break;
            case 505:
                error.message = 'http版本不支持该请求'
                break;
            default:
                error.message = `连接错误${error.response.status}`
        }
    } else {
        // 超时处理
        if (JSON.stringify(error).includes('timeout')) {
            error.message = '服务器响应超时,请刷新当前页'
        } else {
            error.message = '连接服务器失败'
        }
    }

    //提示
    ElMessage.error(error.message)

    /***** 处理结束 *****/
    return Promise.resolve(error)
})

export default axiosInstance
request-wrapper.ts
api文件夹下 创建 api.ts 接口配置
/*
 * @description: 请求接口 配置
 * @Author: Jay
 * @Date: 2023-04-11 13:24:41
 * @LastEditors: Jay
 * @LastEditTime: 2023-09-04 13:30:09
 */

//导入 Axios 请求
import request from '@/utils/request'
//其他配置
import operate from '@/utils/operate';

// 官网接口
export const homePost = (data?: any) => {
    return request({
        url: '/api/index',
        method: 'post',
        data,
        //登录token
        isToken: operate.isToken(),
        //加载动画是否启动
        ifLoading: true,
        //是否允许重复请求
        repeatRequest: false,
    })
}

/*
请求配置与使用

* 请求 方式
    export const 名字 = (data: any) =>
        request.post("接口", data, {
            直接为空
            注:只能配置 AxiosRequestConfig 里有的参数名 可不用配置
        });

*使用 方法
   *引入
        import {
            名字
        } from "../api/api"
    *生命周期中 请求
        名字({请求参数}).then((res) => {
            console.log(res)
        })
*/
api.ts
开始请求
<script lang="ts" setup>
import { onMounted } from "vue";
import { homePost } from "@/api/api";

//生命周期
onMounted(() => {
  homePost().then((res) => {
    console.log("第一次", res);
  });
  homePost().then((res) => {
    console.log("第二次", res);
  });
});
</script>

请求结果

第一次请求被拦截,只有第二次成功返回

3.3 operate.ts 方法

主要放置一些 全局参数与方法。

在页面中可以通过 import operate from "@/utils/operate" 导入使用,也可以在main.ts中全局配置。

/*
 * @description: 全局js
 * @Author: Jay
 * @Date: 2023-09-04 13:53:47
 * @LastEditors: Jay
 * @LastEditTime: 2023-09-04 13:55:44
 */

// vuex 数据
import store from '../store/index'

//接口地址
const baseUrl = () => {
    if (process.env.NODE_ENV == "development") {
        //开发环境
        return "";
    } else {
        //正式环境
        return "";
    }
}

//获取用户token
const isToken = () => {
    if (store.state.Authorization != '') {
        return store.state.Authorization
    }
    return '';
}

/* eslint-disable */

/*
    格式化时间 加上时分秒
    num: 后台时间格式
    type: 'YY-MM-DD' 年月日 ,'HH-MM-SS' 时分秒 ,不传 年月日时分秒
*/
const happenTime = (num: any, type: string) => {
    let date = new Date(num * 1000);
    //时间戳为10位需*1000,时间戳为13位的话不需乘1000
    let y: any = date.getFullYear();
    let MM: any = date.getMonth() + 1;
    MM = MM < 10 ? ('0' + MM) : MM; //月补0
    let d: any = date.getDate();
    d = d < 10 ? ('0' + d) : d; //天补0
    let h: any = date.getHours();
    h = h < 10 ? ('0' + h) : h; //小时补0
    let m: any = date.getMinutes();
    m = m < 10 ? ('0' + m) : m; //分钟补0
    let s: any = date.getSeconds();
    s = s < 10 ? ('0' + s) : s; //秒补0
    if (type === 'YY-MM-DD') {
        //年月日
        return y + '-' + MM + '-' + d;
    } else if (type === 'HH-MM-SS') {
        //时分秒
        return h + ':' + m + ':' + s;
    } else {
        //全部
        return y + '-' + MM + '-' + d + ' ' + h + ':' + m + ':' + s;
    }
}
/* eslint-enable */


// 页面回到顶部(滚动效果)
/*
使用方法
 //监听滚动事件
  window.addEventListener("scroll", proxy.$operate.handleScroll, {
    once: true,
  });
*/
const handleScroll = () => {
    let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
    console.log(scrollTop, "scrollTop");
    if (scrollTop > 0) {
        const timeTop = setInterval(() => {
            document.documentElement.scrollTop = document.body.scrollTop = scrollTop -= 50; //一次减50往上滑动
            if (scrollTop <= 0) {
                clearInterval(timeTop);
            }
        }, 10); //定时调用函数使其更顺滑
    }
};

export default {
    baseUrl,
    isToken,
    happenTime,
    handleScroll
}
operate.ts