Axios封装,学习vben慢慢修改入自己项目

620 阅读5分钟

安装依赖

qs --- 查询参数序列化和解析库。可以将一个普通的object序列化成一个查询字符串,或者反过来将一个查询字符串解析成一个object,而且支持复杂的嵌套

pnpm add qs @types/qs --save-dev 

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

pnpm add lodash-es @types/lodash-es --save-dev

文件目录---组件封装

|--utils
|----http
|------Axios.ts
|------axiosCancel.ts
|------tool.ts
|------types.ts

工具src\utils\http\tool.ts

import type { Recordable } from './types'
/**
 * @description:  是否为函数
 */
// eslint-disable-next-line @typescript-eslint/ban-types
export function isFunction<T = Function>(val: unknown): val is T {
    return is(val, 'Function')
}

/**
 * @description: 判断值是否未某个类型
 */
export function is(val: unknown, type: string) {
    return toString.call(val) === `[object ${type}]`
}

// deepMerge 深度融合
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
    let key: string
    for (key in target) {
        src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key])
    }
    return src
}

/**
 * @description: 是否为对象
 */
export function isObject(val: any): val is Record<any, any> {
    return val !== null && is(val, 'Object')
}

/**
 * @description:  是否为字符串
 */
export function isString(val: unknown): val is string {
    return is(val, 'String')
}

/**
 * 判断是否 url
 * */
const RegExp = /^http(s)?:\/\//iu
export function isUrl(url: string) {
    return RegExp.test(url)
}

/**
 * 将对象添加当作参数拼接到URL上面
 * @param baseUrl 需要拼接的url
 * @param obj 参数对象
 * @returns {string} 拼接后的对象
 * 例子:
 *  let obj = {a: '3', b: '4'}
 *  setObjToUrlParams('www.baidu.com', obj)
 *  ==>www.baidu.com?a=3&b=4
 */
export function setObjToUrlParams(baseUrl: string, obj: object): string {
    let parameters = ''
    let url = ''
    for (const key in obj) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-expect-error
        parameters += `${key}=${encodeURIComponent(obj[key])}&`
    }
    parameters = parameters.replace(/&$/, '')
    if (/\?$/.test(baseUrl)) {
        url = baseUrl + parameters
    } else {
        url = baseUrl.replace(/\/?$/, '?') + parameters
    }
    return url
}
// 加入时间戳
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 }
}

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

/**
 * @description: 格式化请求参数时间
 */
export function formatRequestDate(params: Recordable) {
    if (Object.prototype.toString.call(params) !== '[object Object]') {
        return
    }

    for (const key in params) {
        if (params[key] && params[key]._isAMomentObject) {
            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) {
                    throw new Error(error as any)
                }
            }
        }
        if (isObject(params[key])) {
            formatRequestDate(params[key])
        }
    }
}

数据类型src\utils\http\types.ts

import type { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface CreateAxiosOptions extends AxiosRequestConfig {
    transform?: AxiosTransform
    requestOptions?: RequestOptions
    authenticationScheme?: string
}

export type Recordable<T = any> = Record<string, T>
// 上传文件
export interface UploadFileParams {
    // 其他参数
    data?: Recordable
    // 文件参数接口字段名
    name?: string
    // 文件
    file: File | Blob
    // 文件名称
    filename?: string
    [key: string]: any
}

export interface RequestOptions {
    // 请求参数拼接到url
    joinParamsToUrl?: boolean
    // 格式化请求参数时间
    formatDate?: boolean
    // 是否显示提示信息
    isShowMessage?: boolean
    // 是否解析成JSON
    isParseToJson?: boolean
    // 成功的文本信息
    successMessageText?: string
    // 是否显示成功信息
    isShowSuccessMessage?: boolean
    // 是否显示失败信息
    isShowErrorMessage?: boolean
    // 错误的文本信息
    errorMessageText?: string
    // 是否加入url
    joinPrefix?: boolean
    // 接口地址, 不填则使用默认apiUrl
    apiUrl?: string
    // 请求拼接路径
    urlPrefix?: string
    // 错误消息提示类型
    errorMessageMode?: 'none' | 'modal'
    // 是否添加时间戳
    joinTime?: boolean
    // 不进行任何处理,直接返回
    isTransformResponse?: boolean
    // 是否返回原生响应头
    isReturnNativeResponse?: boolean
    // 忽略重复请求
    ignoreCancelToken?: boolean
    // 是否携带token
    withToken?: boolean
}

export abstract class AxiosTransform {
    /**
     * @description: 1..请求之前处理配置
     * @description: Process configuration before request
     */
    beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig

    /**
     * @description: 2..请求之前的拦截器
     */
    requestInterceptors?: (
        config: AxiosRequestConfig,
        options: CreateAxiosOptions
    ) => AxiosRequestConfig

    /**
     * @description: 3..请求之前的拦截器错误处理
     */
    requestInterceptorsCatch?: (error: Error) => void

    /**
     * @description: 4..请求成功处理
     */
    transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any

    /**
     * @description: 5..请求失败处理
     */
    requestCatch?: (e: Error) => Promise<any>

    /**
     * @description: 6..请求之后的拦截器
     */
    responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>

    /**
     * @description: 7..请求之后的拦截器错误处理
     */
    responseInterceptorsCatch?: (error: Error) => void
}

/**
 * @description: 请求方法
 */
export enum RequestEnum {
    GET = 'GET',
    POST = 'POST',
    PATCH = 'PATCH',
    PUT = 'PUT',
    DELETE = 'DELETE'
}

/**
 * @description:  常用的contentTyp类型
 */
export enum ContentTypeEnum {
    // json
    JSON = 'application/json;charset=UTF-8',
    // json
    TEXT = 'text/plain;charset=UTF-8',
    // form-data 一般配合qs
    FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
    // form-data  上传
    FORM_DATA = 'multipart/form-data;charset=UTF-8'
}

export interface Result<T = any> {
    code: number
    msg: string
    data?: T
}

取消请求src\utils\http\axiosCancel.ts

import type { AxiosRequestConfig, Canceler } from 'axios'
import axios from 'axios'
import qs from 'qs'
import { isFunction } from './tool'

// 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>()

export function getPendingUrl(config: AxiosRequestConfig) {
    return [config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join(
        '&'
    )
}

export class AxiosCanceler {
    /**
     * 添加请求
     * @param {Object} config
     */
    addPending(config: AxiosRequestConfig) {
        this.removePending(config)
        const url = getPendingUrl(config)
        config.cancelToken =
            config.cancelToken ||
            new axios.CancelToken((cancel) => {
                if (!pendingMap.has(url)) {
                    // 如果 pending 中不存在当前请求,则添加进去
                    pendingMap.set(url, cancel)
                }
            })
    }

    /**
     * @description: 清空所有pending
     */
    removeAllPending() {
        pendingMap.forEach((cancel) => {
            cancel && isFunction(cancel) && cancel()
        })
        pendingMap.clear()
    }

    /**
     * 移除请求
     * @param {Object} config
     */
    removePending(config: AxiosRequestConfig) {
        const url = getPendingUrl(config)

        if (pendingMap.has(url)) {
            // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
            const cancel = pendingMap.get(url)
            cancel && cancel(url)
            pendingMap.delete(url)
        }
    }

    /**
     * @description: 重置
     */
    reset(): void {
        pendingMap = new Map<string, Canceler>()
    }
}

核心文件src\utils\http\Axios.ts

import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import qs from 'qs'
import { cloneDeep } from 'lodash-es'
import { AxiosCanceler } from './axiosCancel'
import {
    CreateAxiosOptions,
    RequestOptions,
    Result,
    UploadFileParams,
    ContentTypeEnum,
    RequestEnum
} from './types'
import { isFunction } from './tool'

/**
 * @description:  axios模块
 */
export class VAxios {
    private axiosInstance: AxiosInstance
    private options: CreateAxiosOptions
    // 构建函数
    constructor(options: CreateAxiosOptions) {
        this.options = options
        this.axiosInstance = axios.create(options)
        this.setupInterceptors()
    }

    getAxios(): AxiosInstance {
        return this.axiosInstance
    }

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

    /**
     * @description: 设置通用header
     */
    setHeader(headers: any): void {
        if (!this.axiosInstance) {
            return
        }
        Object.assign(this.axiosInstance.defaults.headers, headers)
    }

    /**
     * @description:   请求方法
     */
    request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        let conf: AxiosRequestConfig = cloneDeep(config)
        const transform = this.getTransform()

        const { requestOptions } = this.options

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

        const { beforeRequestHook, requestCatch, transformRequestData } = transform || {}
        if (beforeRequestHook && isFunction(beforeRequestHook)) {
            conf = beforeRequestHook(conf, opt)
        }

        // 这里重新 赋值成最新的配置
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-expect-error
        conf.requestOptions = opt
        // 支持 FormData
        conf = this.supportFormData(conf)

        return new Promise((resolve, reject) => {
            this.axiosInstance
                .request<any, AxiosResponse<Result>>(conf)
                .then((res: AxiosResponse<Result>) => {
                    // 请求是否被取消
                    const isCancel = axios.isCancel(res)
                    if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
                        try {
                            const ret = transformRequestData(res, opt)
                            resolve(ret)
                        } catch (err) {
                            reject(err || new Error('request error!'))
                        }
                        return
                    }
                    resolve(res as unknown as Promise<T>)
                })
                .catch((e: Error) => {
                    if (requestCatch && isFunction(requestCatch)) {
                        reject(requestCatch(e))
                        return
                    }
                    reject(e)
                })
        })
    }

    /**
     * @description:  创建axios实例
     */
    private createAxios(config: CreateAxiosOptions): void {
        this.axiosInstance = axios.create(config)
    }

    private getTransform() {
        const { transform } = this.options
        return transform
    }

    /**
     * @description:  文件上传
     */
    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>({
            method: 'POST',
            data: formData,
            headers: {
                'Content-type': ContentTypeEnum.FORM_DATA,
                ignoreCancelToken: true
            },
            ...config
        })
    }

    // support form-data
    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' })
        }
    }

    /**
     * @description: 拦截器配置
     */
    private setupInterceptors() {
        const transform = this.getTransform()
        if (!transform) {
            return
        }
        const {
            requestInterceptors,
            requestInterceptorsCatch,
            responseInterceptors,
            responseInterceptorsCatch
        } = transform

        const axiosCanceler = new AxiosCanceler()

        // 请求拦截器配置处理
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-expect-error
        this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
            const { headers: { ignoreCancelToken } = { ignoreCancelToken: false } } = config
            const ignoreCancel =
                ignoreCancelToken !== undefined
                    ? ignoreCancelToken
                    : this.options.requestOptions?.ignoreCancelToken

            !ignoreCancel && 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, responseInterceptorsCatch)
    }
}

前面知识基础的类定义,实际项目使用代码放在下面

# 文件目录---使用封装

  • |--api
  • |----request
  • |------checkStatus.ts
  • |------index.ts
  • |------transform.ts

src\api\request\checkStatus.ts文件 定义返回http状态码处理结果

import { message } from 'antd'
import { logout } from '@/store/token'
export function checkStatus(status: number, error: any): void {
    switch (status) {
        case 400:
            message.error('服务器400错误', error.message)
            console.log(error)
            break
        case 401:
            message.error('服务器级别错误,用户令牌错误,请重新登录')
            logout()
            break
        case 403:
            message.error('服务器级别错误,用户得到授权,但是访问是被禁止的。!')
            break
        case 404:
            message.error('服务器级别错误,网络请求错误,未找到该资源!')
            break
        case 405:
            message.error('服务器级别错误,网络请求错误,请求方法未允许!')
            break
        case 408:
            message.error('服务器级别错误,网络请求超时')
            break
        case 500:
            message.error('服务器级别错误,服务器错误,请联系管理员!')
            break
        case 501:
            message.error('服务器级别错误,网络未实现')
            break
        case 502:
            message.error('服务器级别错误,网络错误')
            break
        case 503:
            message.error('服务器级别错误,服务不可用,服务器暂时过载或维护!')
            break
        case 504:
            message.error('服务器级别错误,网络超时')
            break
        case 505:
            message.error('服务器级别错误,http版本不支持该请求!')
            break
        default:
            message.error('服务器级别错误,未知错误')
            console.log(error)
            break
    }
}

src\api\request\transform.ts文件 定义axios请求拦截器 配置

import type { AxiosResponse } from 'axios'
import type { AxiosTransform, Recordable, Result, RequestOptions } from '@/utils/http/types'
import { RequestEnum } from '@/utils/http/types'
import { message as Admessage } from 'antd'
import {
    isUrl,
    isString,
    joinTimestamp,
    formatRequestDate,
    setObjToUrlParams
} from '@/utils/http/tool'
import { getToken } from '@/store/token'
import { checkStatus } from './checkStatus'
import axios from 'axios'

const transform: AxiosTransform = {
    /**
     * 请求前 配置参数处理
     */
    beforeRequestHook: (config, options) => {
        const {
            apiUrl,
            joinPrefix,
            joinParamsToUrl,
            formatDate,
            joinTime = true,
            urlPrefix
        } = options
        // 请求地址基础链接
        const isUrlStr = isUrl(config.url as string)
        // 加入链接前缀
        if (!isUrlStr && joinPrefix) {
            config.url = `${urlPrefix}${config.url}`
        }

        if (!isUrlStr && apiUrl && isString(apiUrl)) {
            config.url = `${apiUrl}${config.url}`
        }
        const params = config.params || {}
        const data = config.data || false
        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 {
                    // params 是添加到 url 的请求字符串中的,用于 get 请求
                    // 非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: 请求之前的拦截器--OK
     */
    requestInterceptors: (config, options) => {
        const token = getToken()
        if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
            // token前面加不加东西的意思
            // eslint-disable-next-line no-extra-semi
            ;(config as Recordable).headers.Authorization = options.authenticationScheme
                ? `${options.authenticationScheme} ${token}`
                : token
        }
        return config
    },
    /**
     * @description: 请求成功指挥处理返回数据
     */
    transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
        if (!res) {
            return null
        }
        if (res.status !== 200) {
            return null
        }
        const {
            // 是否显示提示信息
            isShowMessage = true,
            // 是否显示失败信息
            isShowErrorMessage = true,
            // 是否显示成功信息
            isShowSuccessMessage = false,
            // 成功的文本信息
            successMessageText = '请求成功',
            // 错误的文本信息
            errorMessageText = '请求失败',
            // 不进行任何处理,直接返回
            isTransformResponse,
            // 是否返回原生响应头
            isReturnNativeResponse
        } = options
        // 不管请求失败还是成功是否显示
        if (isShowMessage) {
            if (res.data.code !== 1) {
                if (isShowErrorMessage) {
                    Admessage.error(res.data.msg)
                    return null
                } else {
                    Admessage.error(errorMessageText)
                    return null
                }
            } else {
                if (isShowSuccessMessage) {
                    Admessage.error(successMessageText)
                    return null
                }
            }
        }

        if (res.data.code !== 1) {
            Admessage.error(`代码逻辑错误,错误原因:${res.data.data.msg}`)
            console.log('代码逻辑错误,错误原因:', res.data.data.msg)
            return res.data.data
        } else {
            if (isReturnNativeResponse) {
                return res
            }
            if (isTransformResponse) {
                return res.data.data
            } else {
                return res.data
            }
        }
    },
    /**
     * @description: 请求成功发生错误触发函数
     */
    responseInterceptorsCatch: (error: any) => {
        // 判断是否取消请求
        const isCancel = axios.isCancel(error)
        if (isCancel) {
            Admessage.error('服务器错误,请求被取消!')
            return
        }
        // 请求返回是否为空
        if (!error.response) {
            Admessage.error('服务器错误,返回结果response为空')
            return
        } else {
            checkStatus(error?.response?.status, error)
        }
    }
    /**
     *
     *
     *
     *
     * 结束的地方
     */
}

export default transform

src\api\request\index.ts文件 定义主体核心内容

import { VAxios } from '@/utils/http/Axios'
import type { CreateAxiosOptions } from '@/utils/http/types'
import { deepMerge } from '@/utils/http/tool'
import transform from './transform'
// 导入环境变量基础地址
const baseURL =
    import.meta.env.VITE_BUILD_MOCK === 'true' ? '/mock' : import.meta.env.VITE_APP_API_BASEURL
const urlPrefix = 'GUWEN'
/**
 * @description: 拦截器配置
 */

function createAxios(opt?: Partial<CreateAxiosOptions>) {
    return new VAxios(
        deepMerge(
            {
                timeout: 10 * 1000,
                // token前面加不加东西的意思
                authenticationScheme: '',

                // 如果是json格式
                headers: { 'Content-Type': 'application/json;charset=UTF-8' },
                // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },

                // 数据处理方式
                transform,
                // 配置项,下面的选项都可以在独立的接口请求中覆盖
                requestOptions: {
                    // 默认将prefix 添加到url
                    joinPrefix: false,
                    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
                    isReturnNativeResponse: false,
                    // 需要对返回数据进行处理
                    isTransformResponse: true,
                    // post请求的时候添加参数到url
                    joinParamsToUrl: false,
                    // 格式化提交参数时间
                    formatDate: true,
                    // 消息提示类型
                    errorMessageMode: 'none',
                    // 接口地址
                    apiUrl: baseURL,
                    // 接口拼接地址
                    urlPrefix,
                    //  是否加入时间戳
                    joinTime: true,
                    // 忽略重复请求
                    ignoreCancelToken: true,
                    // 是否携带token
                    withToken: true
                },
                withCredentials: false
            },
            opt || {}
        )
    )
}

export const http = createAxios()