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

[图片上传失败...(image-407a8-1679880713792)]

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

效果展示:

[图片上传失败...(image-3a8400-1679880713792)]

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

效果展示:

[图片上传失败...(image-28eb50-1679880713792)]

取消重复请求

需解决的问题:

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

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

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

[图片上传失败...(image-91614-1679880713792)]

首先,先在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();
    }
);

效果展示:

[图片上传失败...(image-8b9d85-1679880713792)]

错误重试机制

需解决的问题:

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

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

效果展示:

[图片上传失败...(image-1ba0fd-1679880713792)]

功能配置

需解决的问题:

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

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

效果展示:

([图片上传失败...(image-e2270c-1679880713792)]?)

最后

到这里axios集成已经完成了,完整代码存放在vue3-basic-admin里面,vue3-basic-admin 是一款开源开箱即用的中后台管理系统。基于 Vue3ViteElement-PlusTypeScriptPinia 等主流技术开发,内置许多开箱即用的组件,能快速构建中后台管理系统,目前决定完全开源。点击预览

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容