文章已同步至掘金:https://juejin.cn/post/7207620183308501052
欢迎访问😃,有任何问题都可留言评论哦~
为什么要封装Axios? {#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%B0%81%E8%A3%85axios%EF%BC%9F}
前端发起一个请求,都要考虑以下几种情况:
- 环境配置(本地环境、测试环境、正式环境)?
- 通用请求头配置?
- 请求前参数处理?
- 请求后数据处理?
- 异常处理?
- 请求错误怎么办?重试操作
- 出现重复请求怎么办?
- 接口的日志收集?
- 等等等...
假如每个接口都要配置上面列举的东西,那肯定是不可行的,所以有必要"稍微"封装处理一下,以便于在项目中愉快的使用
开始操作 {#%E5%BC%80%E5%A7%8B%E6%93%8D%E4%BD%9C}
使用到的包如下:
- 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,
}
环境配置? {#%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%EF%BC%9F}
环境配置比较简单,根据项目启动和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
`
通用请求头配置? {#%E9%80%9A%E7%94%A8%E8%AF%B7%E6%B1%82%E5%A4%B4%E9%85%8D%E7%BD%AE%EF%BC%9F}
众所周知,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
`
请求前参数处理 & 请求后数据处理 & 异常处理? {#%E8%AF%B7%E6%B1%82%E5%89%8D%E5%8F%82%E6%95%B0%E5%A4%84%E7%90%86-%26-%E8%AF%B7%E6%B1%82%E5%90%8E%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86-%26-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%EF%BC%9F}
这个直接在请求拦截和响应拦截处处理即可
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
`
重试操作? {#%E9%87%8D%E8%AF%95%E6%93%8D%E4%BD%9C%EF%BC%9F}
如果接口请求失败了,有可能是网络波动导致的,这时候我们可以重新请求,以增强用户体验,(使用到的库: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
`
重复请求怎么办? {#%E9%87%8D%E5%A4%8D%E8%AF%B7%E6%B1%82%E6%80%8E%E4%B9%88%E5%8A%9E%EF%BC%9F}
一般我们在发起请求的时候,如果上一次请求没响应,则下次请求不让执行,
解决这个问题有几种办法:
- 一种是前端页面控制,如果发起请求,则按钮不可点击,主流的第三方库,如: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
接口的日志收集? {#%E6%8E%A5%E5%8F%A3%E7%9A%84%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86%EF%BC%9F}
这个比较简单,直接在对应的【请求拦截】和【错误拦截】处调用收集日志的接口,把对应的数据传递过去即可,这边就不写了,自己加两行代码即可。
结语 {#%E7%BB%93%E8%AF%AD}
至此就可以在项目中愉快的使用了
项目地址( 欢迎 Star,希望动动手指,点个小星星 ):https://github.com/junyangfan/chat
Axios请求封装的文件路径:src -> api -> request.ts