vue3+ts Axios封裝—重複請求攔截

2023-09-04 15:00:19

建立好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 14:52:09
 */

// 匯入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, operate.uploadParameters())
            // 如果要求攜帶在請求頭中
            // 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 '';
}

//上傳請求頭 登入驗證
const uploadParameters = () => {
    return { 'token': isToken() }
    // return { 'Authori-zation': 'Bearer ' + isToken() }
}

/* 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,
    uploadParameters,
    happenTime,
    handleScroll
}
operate.ts