文章已同步至掘金:https://juejin.cn/post/7207620183308501052
欢迎访问😃,有任何问题都可留言评论哦~
为什么要封装Axios?
前端发起一个请求,都要考虑以下几种情况:
- 环境配置(本地环境、测试环境、正式环境)?
- 通用请求头配置?
- 请求前参数处理?
- 请求后数据处理?
- 异常处理?
- 请求错误怎么办?重试操作
- 出现重复请求怎么办?
- 接口的日志收集?
- 等等等…
假如每个接口都要配置上面列举的东西,那肯定是不可行的,所以有必要“稍微”封装处理一下,以便于在项目中愉快的使用
开始操作
使用到的包如下:
- axios(请求主库)
- axios-retry(axios附赠的重试库)
- qs(用来处理一些参数等,不用也可以,可以直接使用JSON.stringfy())
- crypto-js(用来加密等)
前提:
新建一个request.ts
文件来写封装的方法
新建一个config.ts
来放Axios的配置参数
这个是我所有的config.ts
的配置
// config.ts所有的配置
// axios配置
export const axiosConfig: AxiosConfig = {
baseURL_dev: 'http://127.0.0.1:9675', // 测试环境地址
baseURL_prod: '', // 正式环境地址
timeout: 3000, // 超时时间(可以根据不同的环境配置响应时间)
withCredentials: true, // 是否允许携带cookie
retries: 0, // 请求失败重试次数`
shouldResetTimeout: true, // 重试的时候是否重置超时时间
retryDelay: 0, // 每个请求之间的重试延迟时间(ms)`
}
interface AxiosConfig {
baseURL_dev: string
baseURL_prod: string
timeout: number
withCredentials: boolean
retries: number
shouldResetTimeout: boolean
retryDelay: any,
}
环境配置?
环境配置比较简单,根据项目启动和build时的参数,来使用不同的baseURL
即可
// request.ts
import axios from 'axios'
import { axiosConfig } from './config'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 默认导出
export default axiosService
通用请求头配置?
众所周知,Axios有请求拦截axiosService.interceptors.request.use()
和响应拦截axiosService.interceptors.response.use()
,配置通用请求头,只需要在请求拦截的时候加入通用配置即可
// request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { axiosConfig } from './config'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 配置通用请求头
const headers = {
// getLocalLang()是获取项目语言的方法,想把语言给到服务端,然后服务端可以根据不同的语种返回数据
// language: getLocalLang(),
'Content-Type': 'application/json',
// 主要用来处理项目的鉴权,假如我们使用了第三方存储库,例如pinia,redux等,如果想直接在此处获取配置,是有问题的,所以可以动态配置在请求拦截use中
// Authorization: getToken(),
}
axiosService.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
// 自己配置的通用的headers
...headers,
// 默认的headers(接口处传递的herders),传递的默认要覆盖默认的,所以放后面
...config.headers,
}
return config
},
)
// 默认导出
export default axiosService
有人要问,加入在请求拦截处发生错误怎么办?
那我们就加个错误处理函数errorHandler
// request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { axiosConfig } from './config'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 配置通用请求头
const headers = {
// getLocalLang()是获取项目语言的方法,想把语言给到服务端,然后服务端可以根据不同的语种返回数据
// language: getLocalLang(),
'Content-Type': 'application/json',
// 主要用来处理项目的鉴权,假如我们使用了第三方存储库,例如pinia,redux等,如果想直接在此处获取配置,是有问题的,所以可以动态配置在请求拦截use中
// Authorization: getToken(),
}
axiosService.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
// 自己配置的通用的headers
...headers,
// 默认的headers(接口处传递的herders),传递的默认要覆盖默认的,所以放后面
...config.headers,
}
return config
},
// 错误处理
(error) => errorHandler(error)
)
// 错误处理
const errorHandler = (error: any) => {
// const isCusMsg = error.code && errorMessage.findIndex((i) => i.code === error.code) !== -1
// if (isCusMsg) {
// const msg = errorMessage.find((i) => i.code === error.code)?.msg
// message.error(`${error.code},${msg}`)
// return Promise.reject(error)
// }
// message.error(i18n.t('request.error.all.msg'))
// 抛出异常
message.error('请求异常,请稍后重试!')
return Promise.reject(error)
}
// 这边没用自己定义的,全都用后端返回的,如果没返回,则用默认的
// const errorMessage = [
// { code: 400, msg: '错误请求' },
// { code: 401, msg: '未授权,请刷新系统重新登录' },
// { code: 403, msg: '拒绝访问' },
// { code: 404, msg: '请求地址出错' },
// { code: 405, msg: '请求方法未允许' },
// { code: 408, msg: '请求超时' },
// { code: 500, msg: '服务器内部错误' },
// { code: 501, msg: '服务未实现' },
// { code: 502, msg: '网关错误' },
// { code: 503, msg: '服务不可用' },
// { code: 504, msg: '网关超时' },
// { code: 505, msg: 'HTTP版本不受支持' },
// ]
// 默认导出
export default axiosService
请求前参数处理 & 请求后数据处理 & 异常处理?
这个直接在请求拦截和响应拦截处处理即可
PS: 把上一步注释的内容删掉,要不然看着有点多
// request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { axiosConfig } from './config'
import { message } from 'antd'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 配置通用请求头
const headers = {
// getLocalLang()是获取项目语言的方法,想把语言给到服务端,然后服务端可以根据不同的语种返回数据
// language: getLocalLang(),
'Content-Type': 'application/json',
// 主要用来处理项目的鉴权,假如我们使用了第三方存储库,例如pinia,redux等,如果想直接在此处获取配置,是有问题的,所以可以动态配置在请求拦截use中
// Authorization: getToken(),
}
// 请求拦截
axiosService.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
// 自己配置的通用的headers
...headers,
// 默认的headers(接口处传递的herders),传递的默认要覆盖默认的,所以放后面
...config.headers,
}
return config
},
// 错误处理
(error) => errorHandler(error)
)
// 响应拦截
axiosService.interceptors.response.use(
(response: AxiosResponse) => {
const { config, data } = response
// 错误处理(我们的所有接口都会默认返回一个success用来判断成功还是失败)
if (data && !data.success) {
return errorHandler(data)
}
// do something.....
return data
},
// 错误处理
(error) => errorHandler(error)
)
// 错误处理
const errorHandler = (error: any) => {
message.error('请求异常,请稍后重试!')
return Promise.reject(error)
}
// 默认导出
export default axiosService
重试操作?
如果接口请求失败了,有可能是网络波动导致的,这时候我们可以重新请求,以增强用户体验,(使用到的库:axios-retry)
新增一个配置包裹住Axios服务即可
// request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { axiosConfig } from './config'
import { message } from 'antd'
import axiosRetry from 'axios-retry'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 重试操作
axiosRetry(axiosService, {
retries,
shouldResetTimeout,
retryDelay: (retryCount) => retryCount * retryDelay,
retryCondition: (error) => {
// 包含超时,则返回错误
return error.message.includes('timeout')
},
})
// 配置通用请求头
const headers = {
// getLocalLang()是获取项目语言的方法,想把语言给到服务端,然后服务端可以根据不同的语种返回数据
// language: getLocalLang(),
'Content-Type': 'application/json',
// 主要用来处理项目的鉴权,假如我们使用了第三方存储库,例如pinia,redux等,如果想直接在此处获取配置,是有问题的,所以可以动态配置在请求拦截use中,在调用的时候再执行
// Authorization: getToken(),
}
// 请求拦截
axiosService.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
// 自己配置的通用的headers
...headers,
// 默认的headers(接口处传递的herders),传递的默认要覆盖默认的,所以放后面
...config.headers,
}
return config
},
// 错误处理
(error) => errorHandler(error)
)
// 响应拦截
axiosService.interceptors.response.use(
(response: AxiosResponse) => {
const { config, data } = response
// 错误处理(我们的所有接口都会默认返回一个success用来判断成功还是失败)
if (data && !data.success) {
return errorHandler(data)
}
// do something.....
return data
},
// 错误处理
(error) => errorHandler(error)
)
// 错误处理
const errorHandler = (error: any) => {
message.error('请求异常,请稍后重试!')
return Promise.reject(error)
}
// 默认导出
export default axiosService
重复请求怎么办?
一般我们在发起请求的时候,如果上一次请求没响应,则下次请求不让执行,
解决这个问题有几种办法:
- 一种是前端页面控制,如果发起请求,则按钮不可点击,主流的第三方库,如:Antd,Element,都有按钮Loading
- 一种是Axios控制,如果请求没响应,重新发起请求的话,则默认取消下次请求
- 还有比如后端控制等
取消请求又分为两种:
- 取消该次请求(常用于POST请求)
- 取消上次请求(常用于GET)
注:一旦请求打到后端,后端都会执行数据处理,所以POST请求要尤其注意,不能说我一个POST打到后端,然后取消了,这样是有问题的,因为后端已经修改数据了。
Axios取消请求需要一个cancelToken
,
而且每次请求的时候,都要有一个请求列表存放地,用来记录不同的请求,一旦请求成功或失败,都要移除这个请求,防止下次相同的请求发不出去
步骤:
- 获取CancelToken
- 声明一个Map用来存放请求List
- 请求拦截处,如果没有该请求,要把该请求放到Map中,如果列表中有这个请求,则取消这个或者上次请求
- 响应拦截处,则要把本次请求移除
- 一旦发生错误,则把这个请求移除
// request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { axiosConfig } from './config'
import { message } from 'antd'
import axiosRetry from 'axios-retry'
import { MD5 } from 'crypto-js'
import qs from 'qs'
// 判断是否是正式环境
const isProd = () => import.meta.env.VITE_APP_ENV === 'production'
const {
baseURL_dev,
baseURL_prod,
timeout,
withCredentials,
retries,
shouldResetTimeout,
retryDelay,
} = axiosConfig
// 声明CancelToken
const CancelToken = axios.CancelToken
// 请求列表(数据格式:{ key: function(){} })
const pendingReqKeys = new Map()
export enum AxiosCancelReq {
BEFORE = 'before',
AFTER = 'after',
}
// 获取请求的Key,用来保存或移除请求
const getReqKey = (config: AxiosRequestConfig) => {
// 请求方式、请求地址、请求参数生成的字符串来作为是否重复请求的依据(通过MD5加密一下,要不然Key太长了)
const { method, url, params, data } = config
// 不用qs的话,用JSON.stringfy()也可
return MD5(
[method, url, qs.stringify(params), qs.stringify(data)].join('&')
).toString()
}
// 请求拦截调用
const reqIntercept = (config: AxiosRequestConfig) => {
if (!config) {
return
}
// 生成请求Key
const key = getReqKey(config)
// 如果包含取消请求的配置,则执行下面判断并取消对应的请求
if (config.cancelRepeat) {
// 取消之前的请求
if (config.cancelRepeat === AxiosCancelReq.BEFORE) {
// Map里面有请求则取消之前的请求
if (pendingReqKeys.has(key)) {
// 取消请求 & 移除key
pendingReqKeys.get(key)()
pendingReqKeys.delete(key)
}
// 把最新的请求设置进去(cancel是个方法,想取消请求的话,直接调用即可)
config.cancelToken = new CancelToken((cancel) => {
pendingReqKeys.set(key, cancel)
})
}
// 取消之后的请求
if (config.cancelRepeat === AxiosCancelReq.AFTER) {
// 如果请求里面有该请求,则直接取消该次请求(保留上次请求)
if (pendingReqKeys.has(key)) {
return (config.cancelToken = new CancelToken((cancel) => cancel()))
}
pendingReqKeys.set(key, null)
}
}
}
// 响应拦截调用(直接获取Key,移除请求即可)
const rspIntercept = (config: AxiosRequestConfig) => {
if (!config) {
return
}
const key = getReqKey(config)
const fn = pendingReqKeys.get(key)
fn && fn()
pendingReqKeys.delete(key)
}
// 创建axios实例
const axiosService = axios.create({
baseURL: isProd() ? baseURL_prod : baseURL_dev,
timeout,
withCredentials,
})
// 重试操作
axiosRetry(axiosService, {
retries,
shouldResetTimeout,
retryDelay: (retryCount) => retryCount * retryDelay,
retryCondition: (error) => {
// 包含超时,则返回错误
return error.message.includes('timeout')
},
})
// 配置通用请求头
const headers = {
// getLocalLang()是获取项目语言的方法,想把语言给到服务端,然后服务端可以根据不同的语种返回数据
// language: getLocalLang(),
'Content-Type': 'application/json',
// 主要用来处理项目的鉴权,假如我们使用了第三方存储库,例如pinia,redux等,如果想直接在此处获取配置,是有问题的,所以可以动态配置在请求拦截use中,在调用的时候再执行
// Authorization: getToken(),
}
// 请求拦截
axiosService.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
// 自己配置的通用的headers
...headers,
// 默认的headers(接口处传递的herders),传递的默认要覆盖默认的,所以放后面
...config.headers,
}
// config.cancelRepeat
// 字段用来判断是否需要取消重复请求,
// - before取消之前的请求
// - after取消之后的请求
// - 没配置字段或者为undefined则不取消重复请求
reqIntercept(config)
return config
},
// 错误处理
(error) => errorHandler(error)
)
// 响应拦截
axiosService.interceptors.response.use(
(response: AxiosResponse) => {
const { config, data } = response
rspIntercept(config)
// 错误处理(我们的所有接口都会默认返回一个success用来判断成功还是失败)
if (data && !data.success) {
return errorHandler(data)
}
// do something.....
return data
},
// 错误处理
(error) => {
const { response, config } = error
// 响应错误处理
rspIntercept(config)
if (response) {
return errorHandler(error)
}
// 如果是取消请求操作,则不返回错误信息
if (axios.isCancel(error)) {
return
}
return errorHandler(error)
}
)
// 错误处理
const errorHandler = (error: any) => {
message.error('请求异常,请稍后重试!')
return Promise.reject(error)
}
// 默认导出
export default axiosService
接口的日志收集?
这个比较简单,直接在对应的【请求拦截】和【错误拦截】处调用收集日志的接口,把对应的数据传递过去即可,这边就不写了,自己加两行代码即可。
结语
至此就可以在项目中愉快的使用了
项目地址( 欢迎 Star,希望动动手指,点个小星星 ):https://github.com/junyangfan/chat
Axios请求封装的文件路径:src -> api -> request.ts
评论区