目 录CONTENT

文章目录

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

俊阳IT知识库
2023-03-07 / 0 评论 / 3 点赞 / 314 阅读 / 3,763 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-03-16,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
广告

文章已同步至掘金:https://juejin.cn/post/7207620183308501052
欢迎访问😃,有任何问题都可留言评论哦~

为什么要封装Axios?

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

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

request-axios-1

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

开始操作

使用到的包如下:

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

步骤:

  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

接口的日志收集?

这个比较简单,直接在对应的【请求拦截】和【错误拦截】处调用收集日志的接口,把对应的数据传递过去即可,这边就不写了,自己加两行代码即可。

结语

至此就可以在项目中愉快的使用了

项目地址( 欢迎 Star,希望动动手指,点个小星星 ):https://github.com/junyangfan/chat

Axios请求封装的文件路径:src -> api -> request.ts

3

评论区