前言
在前端开发中,Axios 和 Fetch 是最常用的 HTTP 请求库。Axios 提供了更丰富的功能,而 Fetch 是原生的浏览器 API。为了更高的可维护性和复用性。本章实现Axios
和 Fetch
的封装实现错误重试。
request.d.ts 文件
ts
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { FetcherFormProps } from "react-router-dom";
import { FetchModuleOptions } from "vite";
export interface RequestConfig {
show: boolean; // 是否显示msg
loading?: boolean; // 是否开启loading
loadingText?: string; // loading开启文字
message?: string; // 请求成功文案
}
export interface RequestOption extends RequestInit, AxiosRequestConfig {
requestType:"axios" | "fetch"
withToken?: boolean;
prefix?: string;
retryCount?: number;
retryTimeout?: number;
requestHooks?: RequestHooks;
tokenPrefix?: string;
headTokenKey?: string;
data?: any;
url?: string;
retry?: boolean;
retryRequest?: (options: RequestOption, error: any) => any;
file:FormData
}
export interface RequestHooks {
afterRequest?: (response:any,config:RequestConfig) => any;
beforeRequest?: (option:RequestOption,config:RequestConfig) => RequestOption;
}
handleMsg.ts (code码错误以及正常处理)
ts
/**
code码处理
*/
import cache from "@/utils/cache"
import { RequestCodeEnum} from "@/enums/requestEnums"
import { RequestConfig } from "./request"
const handleMsg = (pageData: any, config: RequestConfig) => {
if (pageData.code === RequestCodeEnum.SUCCESS) {
config?.show && window.$msg.success(config.message||pageData.msg || 'ok')
} else if (RequestCodeEnum.TOKEN_INVALID.includes(pageData.code)) {
window.$msg.error(pageData.msg || '请求错误')
cache.remove('token')
location.reload()
} else if (pageData.code === RequestCodeEnum.ServerError) {
window.$msg.error("请稍后重试....")
} else {
window.$msg.error(pageData.msg || '请求错误')
}
}
export default handleMsg
fetch 封装
ts
import handleMsg from './handleMsg'
import { RequestConfig, RequestOption } from './request'
import { RequestMethodsEnum } from '@/enums/requestEnums'
/**
处理拼接地址query传参
*/
const queryString = (params: Record<string, any>): string => {
return Object.keys(params).length > 0
? '?' +
Object.keys(params)
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
: ''
}
/**
处理请求返回内容以及显示提示
response.ok 为true判断是否给提示 并且返回后端传回的内容
response.ok 如果为false的话状态码一般都是错误的直接抛出错误
*/
const handleResponse = async (response: Response, config: RequestConfig): Promise<any> => {
const pageData = await response.json()
if (response.ok) {
handleMsg(pageData, config)
return pageData
} else {
window.$msg.error(pageData.code + ':' + pageData.msg || pageData.code + ':' + '请求错误')
throw new Error(pageData.code + ':' + (pageData.msg || '请求错误'))
}
}
const fetchRequest = async (options: RequestOption, config: RequestConfig): Promise<any> => {
/**
判断是否是get请求在拼接get请求的数据
*/
try {
if (options.method === RequestMethodsEnum.GET) {
options.baseURL = options.baseURL + queryString(options.data)
} else {
options.body = options.file? options.file: JSON.stringify(options.data)
}
// 创建控制器
const controller = new AbortController()
options.signal = controller.signal
// 超时停止请求
const timer = setTimeout(() => {
controller.abort()
}, options.timeout)
/*
可以做一些请求前的操作
*/
const response = await fetch(options.baseURL, options)
clearTimeout(timer)
return await handleResponse(response, config)
} catch (err: any) {
/*
统一捕获一些错误的处理
*/
/**
错误重试
判断是否开启重试 并且重试次数是否小于0 或者 错误重试没开启 直接放回错误信息
*/
window.$msg.error(err.message || '发送请求错误')
if (options.retry && options.retryCount < 0 || !options.retry) {
return new Error(err.message || '发送请求错误')
}
}
/**
如果重试开启并且重试次数大于0开始发请求
*/
if (options.retry && options.retryCount > 0) {
await new Promise(resolve => setTimeout(resolve, options.retryTimeout));
options.retryCount--
return fetchRequest(options, config)
}
}
export default fetchRequest
axios封装
ts
import axios, { AxiosInstance } from 'axios'
import handleMsg from "./handleMsg"
import { RequestOption } from './request'
const createAxiosRequest = (option:RequestOption):AxiosInstance => {
const service = axios.create(option)
// ! 请求拦截器
service.interceptors.request.use(
config => {
config.headers = option.headers
return config
},
error => {
return Promise.reject(error)
}
)
// ! 响应拦截器
service.interceptors.response.use(
response => {
const config = response.config;
const res = response.data
handleMsg(res, config)
return res
},
error => {
window.$msg.error(error.message)
error.config.retryCount--;
if (error.config.retry && error.config.retryCount < 0 || !error.config.retry) {
return new Error(error)
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(service(error.config));
}, error.config.retryTimeout);
})
}
)
return service
}
export default createAxiosRequest
HttpRequest 类
ts
import axios, { AxiosInstance } from "axios";
import { RequestConfig, RequestOption } from './request';
import createAxiosRequest from './axios';
import fetchRequest from "./fetch";
import { RequestMethodsEnum,ContentTypeEnum} from "@/enums/requestEnums"
let axiosReuqest
export default class HttpRequest {
private readonly option: RequestOption;
constructor(option: RequestOption) {
if (typeof option.requestHooks.beforeRequest=== 'function') {
this.option =option.requestHooks.beforeRequest(option)
} else {
throw new Error('requestHooks afterRequest is not a function')
}
if (this.option.requestType === 'axios') {
axiosReuqest = createAxiosRequest(this.option)
}
}
get(option: RequestOption, config: RequestConfig):Promise<any> {
return this.request({...option,method:RequestMethodsEnum.GET}, config)
}
post(option: RequestOption, config: RequestConfig): Promise<any> {
return this.request({...option,method:RequestMethodsEnum.POST}, config)
}
put(option: RequestOption, config: RequestConfig): Promise<any> {
return this.request({...option,method:RequestMethodsEnum.PUT}, config)
}
delete(option: RequestOption, config: RequestConfig): Promise<any> {
return this.request({...option,method:RequestMethodsEnum.DELETE}, config)
}
uploadFile(option: RequestOption, config: RequestConfig) {
option.headers['Content-Type'] = ContentTypeEnum.FORM_DATA
const method = RequestMethodsEnum.POST
if (this.option.requestType === "axios") {
option.data=option.file
return axiosReuqest({
...option,
...config,
method
})
} else {
option.baseURL=this.option.baseURL+option.url
return fetchRequest({
...this.option,
...option,
method
}, config)
}
}
private readonly request(option: RequestOption, config: RequestConfig): Promise<any> {
if (this.option.requestType === 'axios') {
return axiosReuqest({ ...option, ...config })
} else {
option.baseURL=this.option.baseURL+option.url
return fetchRequest({...this.option,...option}, config)
}
}
}
创建Http实例以及使用
ts
import { RequestHooks, RequestOption } from "./request";
import cache from "@/utils/cache";
import { TOKEN_KEY } from "@/enums/cacheEnums"
import { ContentTypeEnum } from "@/enums/requestEnums"
import HttpRequest from "./http";
const requestHooks: RequestHooks = {
beforeRequest: (option, config) => {
let { withToken, headers, prefix, baseURL,tokenPrefix,headTokenKey,credentials,requestType,withCredentials } = option
option.headers = headers ?? {}
if (prefix) {
option.baseURL = baseURL + prefix
}
if (withCredentials && requestType === "fetch") {
option.credentials ="include"
}
if (withToken) {
option.headers[headTokenKey] = tokenPrefix + cache.getCookie(TOKEN_KEY)
}
return option
},
}
const defaultOptions:RequestOption = {
baseURL:import.meta.env.VITE_APP_URL, // 服务器地址
prefix: import.meta.env.VITE_APP_PREFIX, // /api
headers: {
'Content-Type':ContentTypeEnum.JSON,
},
withCredentials :true, // 是否开启cookie
withToken: false, // 是否携带token(headders)
requestHooks:requestHooks, // 请求hooks
requestType: "axios", // 选择请求方式
retryCount: 2, // 重试次数
retryTimeout: 1000, // 重试请求时间
tokenPrefix: "Bearer ", // token前缀
headTokenKey: "Authorization", // token携带key
timeout: 5000, // 请求超时时间
retry: true, // 开启请求重试
};
export const createRequest = (options: RequestOption) => {
return new HttpRequest({
...defaultOptions,
...options
})
}
const request = createRequest()
export default request
使用
ts
import request from "@/utils/request"
//**
get 请求
*/
// quert
cosnt aa =(data:any)=> request.get({url:"/xxxx",data})
// params
const aa = (id:number)=>request.get({url:`/xxxx/${id}`})
/**
post delete put 请求
*/
const aa = (data:any)=> request.post({url:"/xxxx",data},{show:true,message:"xxxx"})
/**
上传图片
*/
const upload = (file:FormData)=>request.uploadFile({url:"/xxx",file},{show:true,message:"上传成功"})