安装依赖
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()