Appearance
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])
}
}
}