前言

最近在写admin我的项目时,想对axios方面进行一个彻底的重造,除了惯例的错误信息拦挡外,减少一些新的性能,目前已实现:loading加载谬误主动重试谬误日志记录勾销反复申请,两头也遇到过一些问题,这里记录下如何解决的,心愿对你有所帮忙。

ps:这里应用的vue3+ts+vite

根底配置

先装置axios:
# 抉择一个你喜爱的包管理器# NPM$ npm install axios -s# Yarn$ yarn add axios# pnpm$ pnpm install axios -s
初始化axios
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";import axios from "axios";const service: AxiosInstance = axios.create({    baseURL: '/api',    timeout: 10 * 1000, // 申请超时工夫    headers: { "Content-Type": "application/json;charset=UTF-8" }});
辨别不同环境

理论我的项目中,咱们可能分开发环境、测试环境和生产环境,所以咱们先在根目录建设三个文件:.env.development .env.production.env.test

其中 vite 内置了几个办法获取环境变量:

如果你想自定义一些变量,必须要以 VITE_结尾,这样才会被vite检测到并读取,例如咱们能够在刚刚建设三种环境下定义title,api等字段,vite会主动依据以后的环境去加载对应的文件。

# development# app titleVITE_APP_Title=Vue3 Basic Admin Dev# baseUrlVITE_BASE_API= /dev# public pathVITE_PUBLIC_PATH = /
# production# app titleVITE_APP_Title=Vue3 Basic Admin# baseUrlVITE_BASE_API= /prod# public pathVITE_PUBLIC_PATH = /
# test# app titleVITE_APP_Title=Vue3 Basic Admin Test# baseUrlVITE_BASE_API= /test# public pathVITE_PUBLIC_PATH = /

批改axios baseUrl:

import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";import axios from "axios";const service: AxiosInstance = axios.create({    baseURL: import.meta.env.VITE_BASE_API,    timeout: 10 * 1000, // 申请超时工夫    headers: { "Content-Type": "application/json;charset=UTF-8" }});
抽取hook

有些时候咱们不心愿我的项目中间接应用 import.meta.env去读取env配置,这样可能会不便于管理,这个时候能够抽取一个公共hook文件,这个hook文件次要就是去读取env配置,而后页面中再通过读取这个hook拿到对应的配置。

还有一个起因就是 如果env配置中文件名字变换了,还得一个个的去我的项目中手动改,比拟麻烦,在hook中能够解构别名,而后return进来即可。

建设 src/hooks/useEnv.ts

export function useEnv() { const { VITE_APP_TITLE, VITE_BASE_API, VITE_PUBLIC_PATH, MODE } = import.meta.env; // 如果名字变换了,咱们能够在这里解构别名    return {        MODE,        VITE_APP_NAME,        VITE_BASE_API,        VITE_PUBLIC_PATH,        VITE_BASE_UPLOAD_API    };}

再更改axios

import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";import axios from "axios";import { useEnv } from "@/hooks";const { VITE_BASE_API } = useEnv();const service: AxiosInstance = axios.create({    baseURL: VITE_BASE_API,    timeout: 10 * 1000, // 申请超时工夫    headers: { "Content-Type": "application/json;charset=UTF-8" }});

响应和拦挡

这里就是陈词滥调的axios申请和相应了,相干的例子有太多了,这里就简略形容下。

# 申请service.interceptors.request.use((config: AxiosRequestConfig) => {    // 这里能够设置token: config!.headers!.Authorization = token    return config;});
# 响应service.interceptors.response.use((response: AxiosResponse) => {        const data = response.data;        if (data.code === 200) {            return data;        } else {            return Promise.reject(data);        }    },    (err) => {        return Promise.reject(err.response);    });

对外裸露办法和应用

内部应用时想通过xx.get间接能申请,所以这里定义一个对外裸露的办法。

// ...后面的省略const request = {    get<T = any>(url: string, data?: any): Promise<T> {        return request.request("GET", url, { params: data });    },    post<T = any>(url: string, data?: any): Promise<T> {        return request.request("POST", url, { data });    },    put<T = any>(url: string, data?: any): Promise<T> {        return request.request("PUT", url, { data });    },    delete<T = any>(url: string, data?: any): Promise<T> {        return request.request("DELETE", url, { params: data });    },    request<T = any>(method = "GET", url: string, data?: any): Promise<T> {        return new Promise((resolve, reject) => {            service({ method, url, ...data })                .then((res) => {                    resolve(res as unknown as Promise<T>);                })                .catch((e: Error | AxiosError) => {                    reject(e);                })        });    }};export default request;

内部应用:

api/user.ts文件中:

import request from "@/utils/request";export const login = (data?: any) => request.post('/login', data);

到这里根底封装曾经实现,接下来咱们给axios增加一些性能。

谬误日志收集

需解决的问题:

  • 收集接口错误信息

所以为了解决这个问题,咱们能够给axios扩大一个log办法,去解决这个问题。

首先,先在axios同级定义log.ts文件:定义addErrorLog办法

  • 当页面进入响应拦截器的且进入error回调的时候,获取以后的url,method,params,data等参数,并申请增加日志接口。
export const addAjaxErrorLog = (err: any) => {    const { url, method, params, data, requestOptions } = err.config;   addErrorLog({        type:'ajax',        url: <string>url,        method,        params: ["get", "delete"].includes(<string>method) ? JSON.stringify(params) : JSON.stringify(data),        data: err.data ? JSON.stringify(err.data) : "",        detail: JSON.stringify(err)    });};

axios中引入应用

import {addAjaxErrorLog} from "./log"# 响应service.interceptors.response.use((response: AxiosResponse) => {       // ...其余代码    },    (err) => {         // ...其余代码        addAjaxErrorLog(err.response)    });

成果展现:

增加loading性能

需解决的问题:

  • 有时候咱们可能须要axios申请时主动开启loading,完结时敞开loading;
  • 初始化同时申请n个接口,申请实现后敞开loading,但又不想页面中应用Primise.all

所以为了解决这两个问题,咱们能够给axios扩大一个loading办法,去解决这两个问题。

首先,先在axios同级定义loading.ts文件:定义initaddclose 三个办法

  • 当进入申请拦截器的时候,调用addLoading,开启loadingloadingCount++
  • 当页面进入响应拦截器的时候,调用closeLoaidng,loadingCount--,如果loadingCount数量为0则敞开loading
import { ElLoading } from 'element-plus'export class AxiosLoading {    loadingCount: number;    loading:any     constructor() {        this.loadingCount = 0;    }        initLoading(){      if(this.loading){         this.loading?.close?.()      }      this.loading=ElLoading.service({          fullscreen:true      })    }    addLoading() {        if (this.loadingCount === 0) {            initLoading()        }        this.loadingCount++;    }    closeLoading() {        if (this.loadingCount > 0) {            if (this.loadingCount === 1) {                loading.close();            }            this.loadingCount--;        }    }}

axios中引入应用:

import {AxiosLoaing} from "./loading"const axiosLoaing=new AxiosLoaing()// ...其余代码# 申请service.interceptors.request.use((config: AxiosRequestConfig) => {    axiosLoaing.addLoading();    return config;});
# 响应service.interceptors.response.use((response: AxiosResponse) => {       // ...其余代码        axiosLoaing.closeLoading();    },    (err) => {         // ...其余代码        axiosLoaing.closeLoading();    });

成果展现:

勾销反复申请

需解决的问题:

  • 有时候可能会同时申请雷同的接口(例如:分页的时候很快的切换几次页码),如果该接口比较慢就比拟节约性能

所以为了解决这个问题,咱们能够给axios扩大一个calcel办法,去解决这个问题。

这里先理解下axios给出的示例,申请接口的时候通过AbortController拿到实例,申请时携带signal,如果想勾销能够通过返回实例的abort办法。

首先,先在axios同级定义calcel.ts文件:定义addPendingremovePending,removeAllPending,reset 四个办法

  • 当进入申请拦截器的时候,调用addPending,调用addPending前,先执行removePedding办法,终止掉申请队列中的申请,pendingMap set以后申请,以method,url参数当做key,valueAbortController实例;
  • 当页面进入响应拦截器的时候,调用remove,革除pendingMap中的以后key
  • 须要留神的是,勾销申请后会触发响应拦截器的err回调,所以须要做一下解决。
import type { AxiosRequestConfig } from "axios";export class AxiosCancel {    pendingMap: Map<string, AbortController>;    constructor() {        this.pendingMap = new Map<string, AbortController>();    }        generateKey(config: AxiosRequestConfig): string {        const { method, url } = config;        return [ url || "", method || ""].join("&");    }    addPending(config: AxiosRequestConfig) {        this.removePending(config);        const key: string = this.generateKey(config);        if (!this.pendingMap.has(key)) {            const controller = new AbortController();            config.signal = controller.signal;            this.pendingMap.set(key, controller);        } else {            config.signal = (this.pendingMap.get(key) as AbortController).signal;        }    }    removePending(config: AxiosRequestConfig) {        const key: string = this.generateKey(config);        if (this.pendingMap.has(key)) {            (this.pendingMap.get(key) as AbortController).abort();            this.pendingMap.delete(key);        }    }    removeAllPending() {        this.pendingMap.forEach((cancel: AbortController) => {            cancel.abort();        });        this.reset();    }    reset() {        this.pendingMap = new Map<string, AbortController>();    }}

axios中引入应用:

import {AxiosCancel} from "./cancel"const axiosCancel=new AxiosCancel()// ...其余代码# 申请service.interceptors.request.use((config: AxiosRequestConfig) => {    axiosCancel.addPending();    return config;});
# 响应service.interceptors.response.use((response: AxiosResponse) => {       // ...其余代码        axiosCancel.removePending();    },    (err) => {         if (err.code === "ERR_CANCELED") return;        // ...其余代码        axiosCancel.removePending();    });

成果展现:

谬误重试机制

需解决的问题:

  • 有时候接口可能忽然出问题,所以容许谬误主动重试 (慎用!!!)

所以为了解决这个问题,咱们能够给axios扩大一个retry办法,去解决这个问题。

首先,先在axios同级定义retry.ts文件:定义retry办法

  • 当页面进入响应拦截器的且进入error回调的时候,判断以后接口的目前的重试次数是否大于规定的重试次数,如果小于,则执行retry办法进行接口从新申请。
import type { AxiosError, AxiosInstance } from "axios";export class AxiosRetry {    retry(service: AxiosInstance, err: AxiosError) {        const config = err?.config as any;        config._retryCount = config._retryCount || 0;        config._retryCount += 1;        delete config.headers;  //删除config中的header,采纳默认生成的header        setTimeout(() => {            service(config);        }, 100);    }}

axios中引入应用:

import {AxiosRetry} from "./retry"

响应

service.interceptors.response.use((response: AxiosResponse) => {       // ...其余代码    },    (err) => {          if ((err.config._retryCount || 0) < 3) {            const axiosRetry = new AxiosRetry();            axiosRetry.retry(service, err);            return;        }        // ...其余代码    });

成果展现:

性能配置

需解决的问题:

  • 有时候可能某个接口仅须要局部性能,例如仅某个接口须要重试,其余的不须要的状况。

所以为了解决这个问题,咱们能够给axios减少了一个默认配置axiosOptions,去解决这个问题。

  • 当页面进入须要应用某些参数的时候,先去读以后接口是否传递了,如果没有则去读取axios默认配置。

设置默认配置:

interface axiosConfig {    successMessage?: boolean;  // 是否提醒胜利信息    errorMessage?: boolean;    // 是否提醒失败信息    cancelSame?: boolean;      // 是否勾销雷同接口    retryCount?: number;       // 失败重试次数    isRetry?: boolean;         // 是否失败重试}const defaultConfig: axiosConfig = {    successMessage: false,    errorMessage: true,    cancelSame: false,    isRetry: false,    retryCount: 3};

批改request,加上requestOptions参数:

# 批改request办法const request = {    request<T = any>(method = "GET", url: string, data?: any, config?: axiosConfig): Promise<T> {               // 和默认配置合并        const options = Object.assign({}, defaultConfig, config);        return new Promise((resolve, reject) => {            service({ method, url, ...data, requestOptions: options })                .then((res) => {                    // ...其余代码                })                .catch((e: Error | AxiosError) => {                  // ...其余代码                })        });    }};

申请拦截器:

service.interceptors.request.use((config: AxiosRequestConfig) => {    const { cancelSame } = config.requestOptions;    if (cancelSame) {        axiosCancel.addPending(config);    }    axiosLoading.addLoading();    return config;});

响应拦截器:

service.interceptors.response.use((response: AxiosResponse) => {       const { cancelSame } = response.config.requestOptions;       if (cancelSame) {        axiosCancel.removePending(response.config);       }       // ...其余代码    },    (err) => {                const { isRetry, retryCount,cancelSame } = err.config.requestOptions;        if (isRetry && (err.config._retryCount || 0) < retryCount) {            //...其余代码        }        cancelSame && axiosCancel.removePending(err.config || {});        // ...其余代码    });

应用:

export const sameTestApi = (data?: any) => request.get('/test', data, { cancelSame: true });

成果展现:

(?)

最初

到这里axios集成曾经实现了,残缺代码寄存在vue3-basic-admin外面,vue3-basic-admin 是一款开源开箱即用的中后盾管理系统。基于 Vue3ViteElement-PlusTypeScriptPinia 等支流技术开发,内置许多开箱即用的组件,能疾速构建中后盾管理系统,目前决定齐全开源。点击预览