Skip to content

responseCode码

ts
export enum ResultEnum {
    SUCCESS,
    SUCCESS_CODE = 200,
    PARAMS_ERROR,
    BACK_END_ERROR = -1, // 后端逻辑错误
    FREQUENT_REQUESTS = 100, // 频繁请求
    SDX_UPGRADE = 1405, // 登录成功
    NOT_FIRST_LOGIN = 20002, // 不是第一次登录
    LOGIN_ERROR = 20401, // 请求密码错误
    EXPIRED = 20402, // 账号到期
    PAUSE = 20403, // 账号停用
    NO_ACCESS = 20405, // 请求没有权限, 比如医院 A 修改医院 B 的订单
    NO_USER_INFO = 20406, // 请求没有该用户的信息
    TOKEN_EMPTY = 50001,
    TOKEN_NO_ACCESS = 50002,
    TOKEN_ERROR = 50008,
    TOKEN_LOGIN = 50013,
    TOKEN_EXPIRATION = 50014,
    TOKEN_SIGNATURE = 50015,
    TOKEN_IGNORE_REQUEST = 50016
}
ts
import { ResultEnum } from '@/enums/httpEnum'
// 成功
export const successList = [
    ResultEnum.SUCCESS,
    ResultEnum.NOT_FIRST_LOGIN,
    ResultEnum.SDX_UPGRADE,
    ResultEnum.SUCCESS_CODE
]
// 错误提示
export const errorList = [
    ResultEnum.PARAMS_ERROR,
    ResultEnum.BACK_END_ERROR,
    ResultEnum.LOGIN_ERROR,
    ResultEnum.PARAMS_ERROR,
    ResultEnum.FREQUENT_REQUESTS,
    ResultEnum.NO_ACCESS
]
// token错误 -> 需要重新登录
export const tokenErrorList = [
    ResultEnum.TOKEN_EMPTY,
    ResultEnum.TOKEN_NO_ACCESS,
    ResultEnum.TOKEN_ERROR,
    ResultEnum.TOKEN_LOGIN,
    ResultEnum.TOKEN_EXPIRATION,
    ResultEnum.TOKEN_SIGNATURE,
    ResultEnum.TOKEN_IGNORE_REQUEST
]
export const messageBoxErrorList = [ResultEnum.PAUSE, ResultEnum.EXPIRED, ResultEnum.NO_USER_INFO]

响应状态码 -> 根据后端规则定制

index

ts
import type { AxiosInstance, AxiosResponse } from 'axios'
import axios from 'axios'
import { clone } from 'lodash-es'

import { ContentTypeEnum, RequestEnum } from '@/enums/httpEnum'
import { useUserStore } from '@/store/modules/user'
import { deepMerge, setObjToUrlParams } from '@/utils'
import { getToken } from '@/utils/auth'
import { isString } from '@/utils/is'
import type { RequestOptions, Result } from '#/axios'

import { VAxios } from './Axios'
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform'
import { formatRequestDate, joinTimestamp } from './helper'
import { errorList, messageBoxErrorList, successList, tokenErrorList } from './responseCodeList'

/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
    /**
     * @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
     */
    transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
        const { isTransformResponse, isReturnNativeResponse } = options

        // 是否返回原生响应头 比如:需要获取响应头时使用该属性
        if (isReturnNativeResponse) {
            return res
        }
        // 不进行任何处理,直接返回
        // 用于页面代码可能需要直接获取code,data,message这些信息时开启
        if (!isTransformResponse) {
            return res.data
        }
        // 错误的时候返回
        const { data } = res
        if (!data) {
            throw new Error('请求出错,请稍候重试')
        }
        //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
        const { code, message } = data
        if (!code) return data // 获取用户信息接口没code
        if (successList.includes(code)) {
            return data
        }
        if (errorList.includes(code)) {
            ElMessage.warning(message || '请求出错,请稍候重试')
            throw new Error(message || '请求出错,请稍候重试')
        }
        if (messageBoxErrorList.includes(code)) {
            ElMessageBox.confirm(message, '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            })
            return
        }
    },
    /**
     * @description: 请求之前处理config
     */
    beforeRequestHook: (config, options) => {
        const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options
        // if (joinPrefix) {
        //   config.url = `${urlPrefix}${config.url}`;
        // }

        if (apiUrl && isString(apiUrl)) {
            config.url = `${apiUrl}${config.url}`
        }
        const params = config.params || {}
        const data = config.data || false
        formatDate && data && !isString(data) && formatRequestDate(data)
        if (config.method?.toUpperCase() === RequestEnum.GET) {
            if (!isString(params)) {
                // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
                config.params = Object.assign(params || {}, joinTimestamp(joinTime, false))
            } else {
                // 兼容restful风格
                config.url = config.url + params + `${joinTimestamp(joinTime, true)}`
                config.params = undefined
            }
        } else {
            if (!isString(params)) {
                formatDate && formatRequestDate(params)
                if (
                    Reflect.has(config, 'data') &&
                    config.data &&
                    (Object.keys(config.data).length > 0 || config.data instanceof FormData)
                ) {
                    config.data = data
                    config.params = params
                } else {
                    // 非GET请求如果没有提供data,则将params视为data
                    config.data = params
                    config.params = undefined
                }
                if (joinParamsToUrl) {
                    config.url = setObjToUrlParams(config.url as string, Object.assign({}, config.params, config.data))
                }
            } else {
                // 兼容restful风格
                config.url = config.url + params
                config.params = undefined
            }
        }
        return config
    },

    /**
     * @description: 请求拦截器处理
     */
    requestInterceptors: (config, options) => {
        const token = getToken()
        if (token) {
            config.headers['token'] = token
        }
        return config
    },

    /**
     * @description: 响应拦截器处理
     */
    responseInterceptors: (res: AxiosResponse<any>) => {
        return res
    },
    /**
     * @description: 响应错误处理
     */
    responseInterceptorsCatch: (axiosInstance: AxiosInstance, error: any) => {
        const { response, code, message, config } = error || {}
        const err: string = error?.toString?.() ?? ''
        if (axios.isCancel(error)) {
            return Promise.reject(error)
        }
        try {
            if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
                ElMessage.warning('请求超时,请刷新页面后重试')
            } else if (err?.includes('Network Error')) {
                ElMessage.warning('网络异常,请检查您的网络连接是否正常')
            } else {
                ElMessage.error(message)
            }
        } catch (error) {
            throw new Error(error as unknown as string)
        }
        if (response && tokenErrorList.includes(response.data.code)) {
            ElMessageBox.confirm('您可能已经在其它页面退出,如需继续访问当前页面,请重新登录', '确定登出', {
                confirmButtonText: '重新登录',
                type: 'warning'
            }).then(() => {
                useUserStore().logout()
            })
        }
    }
}
function createAxios(opt?: Partial<CreateAxiosOptions>) {
    return new VAxios(
        deepMerge(
            {
                authenticationScheme: '',
                timeout: 10 * 1000,
                // 基础接口地址
                baseURL: import.meta.env.VITE_APP_BASE_API1,

                headers: { 'Content-Type': ContentTypeEnum.JSON },
                // 如果是form-data格式
                // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
                // 数据处理方式
                transform: clone(transform),
                // 配置项,下面的选项都可以在独立的接口请求中覆盖
                requestOptions: {
                    // 默认将prefix 添加到url
                    joinPrefix: true,
                    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
                    isReturnNativeResponse: false,
                    // 需要对返回数据进行处理
                    isTransformResponse: true,
                    // post请求的时候添加参数到url
                    joinParamsToUrl: false,
                    // 格式化提交参数时间
                    formatDate: true,
                    // 消息提示类型
                    errorMessageMode: 'message',
                    // 接口地址
                    // apiUrl: import.meta.env.VITE_APP_BASE_API1,
                    //  是否加入时间戳
                    joinTime: true,
                    // 忽略重复请求
                    ignoreCancelToken: true,
                    // 是否携带token
                    withToken: true,
                    retryRequest: {
                        isOpenRetry: true,
                        count: 5,
                        waitTime: 100
                    }
                }
            },
            opt || {}
        )
    )
}

export const request = createAxios()

Axios

ts
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'
import { cloneDeep } from 'lodash-es'
import qs from 'qs'

import { ContentTypeEnum, RequestEnum } from '@/enums/httpEnum'
import { isFunction } from '@/utils/is'
import type { RequestOptions, Result, UploadFileParams } from '#/axios'

import { AxiosCanceler } from './axiosCancel'
import type { CreateAxiosOptions } from './axiosTransform'

export * from './axiosTransform'

/**
 * @description:  VAxios类提供了一种封装和定制axios实例的方法,支持请求和响应的拦截器、请求头设置、文件上传等功能。
 */
export class VAxios {
    private axiosInstance: AxiosInstance
    private readonly options: CreateAxiosOptions

    /**
     * @description 初始化VAxios实例。
     * @param options 创建axios实例的配置选项。
     */
    constructor(options: CreateAxiosOptions) {
        this.options = options
        this.axiosInstance = axios.create(options)
        this.setupInterceptors()
    }

    /**
     * @description:  创建axios实例并替换当前的axiosInstance
     * @param config 创建axios实例的配置选项
     */
    private createAxios(config: CreateAxiosOptions): void {
        this.axiosInstance = axios.create(config)
    }
    /**
     * @description: 获取请求和响应转换函数
     */
    private getTransform() {
        const { transform } = this.options
        return transform
    }
    /**
     * @description: 获取内部的axios实例
     * @returns 返回当前的axios实例
     */
    getAxios(): AxiosInstance {
        return this.axiosInstance
    }

    /**
     * @description: 重新配置axios实例
     * @param config 新的配置选项
     */
    configAxios(config: CreateAxiosOptions) {
        if (!this.axiosInstance) {
            return
        }
        this.createAxios(config)
    }

    /**
     * @description: 设置axios请求头
     * @param headers 要设置的请求头对象
     */
    setHeader(headers: any): void {
        if (!this.axiosInstance) {
            return
        }
        Object.assign(this.axiosInstance.defaults.headers, headers)
    }

    /**
     * @description: 配置axios请求和响应拦截器
     */
    private setupInterceptors() {
        // const transform = this.getTransform();
        // 获取拦截器对象和axios实例
        const {
            axiosInstance,
            options: { transform }
        } = this
        if (!transform) {
            return
        }
        // 获取拦截器
        const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
            transform

        const axiosCanceler = new AxiosCanceler()

        // 请求拦截器配置
        this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
            // 如果启用了取消重复请求的功能,则禁止重复请求

            //获取options
            const requestOptions = (config as unknown as any).requestOptions ?? this.options.requestOptions
            // 获取cancelToken
            const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true
            // 是否取消重复请求
            !ignoreCancelToken && axiosCanceler.addPending(config)

            if (requestInterceptors && isFunction(requestInterceptors)) {
                config = requestInterceptors(config, this.options)
            }
            return config
        }, undefined)

        // 请求拦截器错误捕获
        requestInterceptorsCatch &&
            isFunction(requestInterceptorsCatch) &&
            this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch)

        // 响应拦截器
        this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
            res && axiosCanceler.removePending(res.config)
            if (responseInterceptors && isFunction(responseInterceptors)) {
                res = responseInterceptors(res)
            }
            return res
        }, undefined)

        // 响应拦截器错误捕获
        responseInterceptorsCatch &&
            isFunction(responseInterceptorsCatch) &&
            this.axiosInstance.interceptors.response.use(undefined, (error) => {
                return responseInterceptorsCatch(axiosInstance, error)
            })
    }

    /**
     * @description:  文件上传方法
     * @param config Axios请求配置。
     * @param params 包含文件和额外数据的参数对象。
     * @returns 返回一个Promise对象,用于处理上传结果。
     */
    uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
        const formData = new window.FormData()
        const customFilename = params.name || 'file'

        if (params.filename) {
            formData.append(customFilename, params.file, params.filename)
        } else {
            formData.append(customFilename, params.file)
        }

        if (params.data) {
            Object.keys(params.data).forEach((key) => {
                const value = params.data![key]
                if (Array.isArray(value)) {
                    value.forEach((item) => {
                        formData.append(`${key}[]`, item)
                    })
                    return
                }

                formData.append(key, params.data![key])
            })
        }

        return this.axiosInstance.request<T>({
            ...config,
            method: 'POST',
            data: formData,
            headers: {
                'Content-type': ContentTypeEnum.FORM_DATA,
                // @ts-ignore
                ignoreCancelToken: true
            }
        })
    }

    /**
     * 支持以form-data格式发送数据。
     * @param config Axios请求配置。
     * @returns 返回处理后的Axios请求配置。
     */
    supportFormData(config: AxiosRequestConfig) {
        const headers = config.headers || this.options.headers
        const contentType = headers?.['Content-Type'] || headers?.['content-type']

        if (
            contentType !== ContentTypeEnum.FORM_URLENCODED ||
            !Reflect.has(config, 'data') ||
            config.method?.toUpperCase() === RequestEnum.GET
        ) {
            return config
        }

        return {
            ...config,
            data: qs.stringify(config.data, { arrayFormat: 'brackets' })
        }
    }

    /**
     * 发送GET请求。
     * @param config Axios请求配置。
     * @param options 额外的请求选项。
     * @returns 返回一个Promise对象,用于处理请求结果。
     */
    get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'GET' }, options)
    }

    post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'POST' }, options)
    }

    put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'PUT' }, options)
    }

    delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'DELETE' }, options)
    }

    request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        let conf: CreateAxiosOptions = cloneDeep(config)
        // cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
        if (config.cancelToken) {
            conf.cancelToken = config.cancelToken
        }

        if (config.signal) {
            conf.signal = config.signal
        }

        const transform = this.getTransform()

        const { requestOptions } = this.options

        const opt: RequestOptions = Object.assign({}, requestOptions, options)

        const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {}
        if (beforeRequestHook && isFunction(beforeRequestHook)) {
            conf = beforeRequestHook(conf, opt)
        }
        conf.requestOptions = opt

        conf = this.supportFormData(conf)

        return new Promise((resolve, reject) => {
            this.axiosInstance
                .request<any, AxiosResponse<Result>>(conf)
                .then((res: AxiosResponse<Result>) => {
                    if (transformResponseHook && isFunction(transformResponseHook)) {
                        try {
                            const ret = transformResponseHook(res, opt)
                            resolve(ret)
                        } catch (err) {
                            reject(err || new Error('request error!'))
                        }
                        return
                    }
                    resolve(res as unknown as Promise<T>)
                })
                .catch((e: Error | AxiosError) => {
                    if (requestCatchHook && isFunction(requestCatchHook)) {
                        reject(requestCatchHook(e, opt))
                        return
                    }
                    if (axios.isAxiosError(e)) {
                        // rewrite error message from axios in here
                    }
                    reject(e)
                })
        })
    }
}

axiosTransform

ts
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'

import type { RequestOptions, Result } from '#/axios'

/**
 * @description 创建axios实例时可用的选项接口
 */
export interface CreateAxiosOptions extends AxiosRequestConfig {
    authenticationScheme?: string
    transform?: AxiosTransform
    requestOptions?: RequestOptions
}
/**
 * @description 定义一个抽象类,用于实现请求和响应的转换逻辑
 */
export abstract class AxiosTransform {
    /**
     * @description 在发送请求之前调用的函数。它可以根据需要修改请求配置。
     * @param {AxiosRequestConfig}  请求配置
     * @param {RequestOptions} 请求选项
     * @returns {AxiosRequestConfig} 返回修改后的请求配置。
     */
    beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig

    /**
     * @description 处理响应数据
     * @param {AxiosResponse<Result>}  响应对象
     * @param {RequestOptions}  请求选项
     * @returns 任意类型的处理结果。
     */
    transformResponseHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any

    /**
     * @description: 请求失败处理
     * @param {Error} 错误对象。
     * @param {RequestOptions} 请求选项
     * @returns {Promise<any>} 返回一个Promise对象
     */
    requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>

    /**
     * @description: 请求之前的拦截器
     * @param {InternalAxiosRequestConfig} 内部请求配置
     * @param {CreateAxiosOptions} 创建Axios实例时的选项
     * @returns {InternalAxiosRequestConfig} 返回修改后的内部请求配置
     */
    requestInterceptors?: (
        config: InternalAxiosRequestConfig,
        options: CreateAxiosOptions
    ) => InternalAxiosRequestConfig

    /**
     * @description: 请求之后的拦截器
     * @param {AxiosResponse<any>} 响应对象
     * @returns {AxiosResponse<any>} 返回处理后的响应对象
     */
    responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>

    /**
     * @description: 请求之前的拦截器错误处理
     * @param {Error} 错误对象
     */
    requestInterceptorsCatch?: (error: Error) => void

    /**
     * @description: 请求之后的拦截器错误处理
     * @param {AxiosInstance} Axios实例
     * @param {Error} error 错误对象
     */
    responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: Error) => void
}

axiosCancel

ts
import type { AxiosRequestConfig } from 'axios'

// 用于存储每个请求的标识和取消函数
const pendingMap = new Map<string, AbortController>()

const getPendingUrl = (config: AxiosRequestConfig): string => {
    // 根据请求方法和URL生成唯一的标识
    return [config.method, config.url].join('&')
}

/**
 * @description AxiosCanceler 类用于管理请求的取消逻辑。
 */
export class AxiosCanceler {
    /**
     * 添加一个请求到等待列表中
     * @param config 请求配置对象,包括请求方法和URL等信息。
     */
    public addPending(config: AxiosRequestConfig): void {
        // 先尝试移除之前可能存在的相同请求
        this.removePending(config)
        const url = getPendingUrl(config)
        const controller = new AbortController()
        // 为请求配置对象设置AbortController的signal,用于后续取消请求
        config.signal = config.signal || controller.signal
        if (!pendingMap.has(url)) {
            // 如果当前请求不在等待中,将其添加到等待中
            pendingMap.set(url, controller)
        }
    }

    /**
     * 清除所有等待中的请求,即取消这些请求
     */
    public removeAllPending(): void {
        pendingMap.forEach((abortController) => {
            if (abortController) {
                abortController.abort()
            }
        })
        this.reset()
    }

    /**
     * 移除一个等待中的请求,即取消该请求
     * @param config 请求配置
     */
    public removePending(config: AxiosRequestConfig): void {
        const url = getPendingUrl(config)
        if (pendingMap.has(url)) {
            // 如果当前请求在等待中,取消它并将其从等待中移除
            const abortController = pendingMap.get(url)
            if (abortController) {
                abortController.abort(url)
            }
            pendingMap.delete(url)
        }
    }

    /**
     * 重置 -> 清空所有等待中的请求。
     */
    public reset(): void {
        pendingMap.clear()
    }
}

helper

ts
import { isObject, isString } from '@/utils/is'

const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'

export function joinTimestamp<T extends boolean>(join: boolean, restful: T): T extends true ? string : object

export function joinTimestamp(join: boolean, restful = false): string | object {
    if (!join) {
        return restful ? '' : {}
    }
    const now = new Date().getTime()
    if (restful) {
        return `?_t=${now}`
    }
    return { _t: now }
}

/**
 * @description: Format request parameter time
 */
export function formatRequestDate(params: Recordable) {
    if (Object.prototype.toString.call(params) !== '[object Object]') {
        return
    }

    for (const key in params) {
        const format = params[key]?.format ?? null
        if (format && typeof format === 'function') {
            params[key] = params[key].format(DATE_TIME_FORMAT)
        }
        if (isString(key)) {
            const value = params[key]
            if (value) {
                try {
                    params[key] = isString(value) ? value.trim() : value
                } catch (error: any) {
                    throw new Error(error)
                }
            }
        }
        if (isObject(params[key])) {
            formatRequestDate(params[key])
        }
    }
}