51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

手摸手一步步带你封装Axios(环境区分、通用参数配置、异常处理、请求重试、移除重复请求,错误日志收集)

文章已同步至掘金: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}

前端发起一个请求,都要考虑以下几种情况:

  • 环境配置(本地环境、测试环境、正式环境)?
  • 通用请求头配置?
  • 请求前参数处理?
  • 请求后数据处理?
  • 异常处理?
  • 请求错误怎么办?重试操作
  • 出现重复请求怎么办?
  • 接口的日志收集?
  • 等等等...

request-axios-1

假如每个接口都要配置上面列举的东西,那肯定是不可行的,所以有必要"稍微"封装处理一下,以便于在项目中愉快的使用

开始操作 {#%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
而且每次请求的时候,都要有一个请求列表存放地,用来记录不同的请求,一旦请求成功或失败,都要移除这个请求,防止下次相同的请求发不出去

步骤:

  1. 获取CancelToken

  2. 声明一个Map用来存放请求List

  3. 请求拦截处,如果没有该请求,要把该请求放到Map中,如果列表中有这个请求,则取消这个或者上次请求

  4. 响应拦截处,则要把本次请求移除

  5. 一旦发生错误,则把这个请求移除

    // 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

赞(0)
未经允许不得转载:工具盒子 » 手摸手一步步带你封装Axios(环境区分、通用参数配置、异常处理、请求重试、移除重复请求,错误日志收集)