likes
comments
collection
share

十六.vue3之axios封装集成

作者站长头像
站长
· 阅读数 16

前言

最近在写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 内置了几个方法获取环境变量:

十六.vue3之axios封装集成

如果你想自定义一些变量,必须要以 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.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)
    }
);

效果展示:

十六.vue3之axios封装集成

添加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();
    }
);

效果展示:

十六.vue3之axios封装集成

取消重复请求

需解决的问题:

  • 有时候可能会同时请求相同的接口(例如:分页的时候很快的切换几次页码),如果该接口比较慢就比较浪费性能

所以为了解决这个问题,我们可以给axios扩展一个calcel方法,去解决这个问题。

这里先了解下axios给出的示例,请求接口的时候通过AbortController拿到实例,请求时携带signal,如果想取消可以通过返回实例的abort方法。

十六.vue3之axios封装集成

首先,先在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) => {
       // ...其他代码
        axiosLoaing.removePending();
    },
    (err) => {
         if (err.code === "ERR_CANCELED") return;
        // ...其他代码
        axiosLoaing.removePending();
    }
);

效果展示:

十六.vue3之axios封装集成

错误重试机制

需解决的问题:

  • 有时候接口可能突然出问题,所以允许错误自动重试 (慎用!!!)

所以为了解决这个问题,我们可以给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"

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

效果展示:

十六.vue3之axios封装集成

功能配置

需解决的问题:

  • 有时候可能某个接口仅需要部分功能,例如仅某个接口需要重试,其他的不需要的情况。

所以为了解决这个问题,我们可以给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 });

效果展示:

十六.vue3之axios封装集成

最后

到这里axios集成已经完成了,完整代码这里就不贴了,后续代码会在vue3-basic-admin上,使用vue3+ts+pinia+vite开发的后台管理系统,很多开箱即用的组件,敬请期待。

如果觉得本文对你有帮助,可以点一下赞,如果好的意见可以评论区留言,大家共同进步。

其他文章