乐趣区

关于axios:vue3之axios封装集成

前言

最近在写 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 title
VITE_APP_Title=Vue3 Basic Admin Dev

# baseUrl
VITE_BASE_API= /dev

# public path
VITE_PUBLIC_PATH = /
# production

# app title
VITE_APP_Title=Vue3 Basic Admin

# baseUrl
VITE_BASE_API= /prod

# public path
VITE_PUBLIC_PATH = /
# test

# app title
VITE_APP_Title=Vue3 Basic Admin Test

# baseUrl
VITE_BASE_API= /test

# public path
VITE_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.ge t 间接能申请,所以这里定义一个对外裸露的办法。

// ... 后面的省略
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 等支流技术开发,内置许多开箱即用的组件,能疾速构建中后盾管理系统,目前决定齐全开源。点击预览

退出移动版