结合TS实现axios按业务逻辑按需进行二次封装

62 阅读3分钟

本篇短文主要总结在项目中使用的第三方请求库axios结合TS实现的二次封装,实现了对请求和响应对具体业务的封装配置,代码粘贴及项目项目相关结构如下

  1. src目录下新建service目录

image.png

  1. request.ts代码
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import axios from 'axios'

import notify from './interceptor/notify'
import limiter from './interceptor/limiter'
import urlArgs from './interceptor/url-args'

const instance = axios.create()
instance.interceptors.request.use((request: any) => {
  request.headers.Authorization = localStorage.getItem('authorization')
  return request
})
instance.interceptors.request.use(urlArgs.request.onFulfilled, undefined)
instance.interceptors.request.use(limiter.request.onFulfilled, undefined)
instance.interceptors.response.use(
  notify.response.onFulfilled,
  notify.response.onRejected
)
instance.interceptors.response.use(
  limiter.response.onFulfilled,
  limiter.response.onRejected
)

/**
 * @description 先不写类型,因为比较繁琐
 */
export function compose(adapters: any[]) {
  return adapters.reduceRight((pre, cur) => cur(pre))
}
//处理返回code为非0的错误
export class CodeNotZeroError extends Error {
  code: number

  constructor(code: number, message: string) {
    super(message)
    this.code = code
  }
}
//返回结果格式处理
export interface ResultFormat<T = any> {
  data: null | T
  err: AxiosError | CodeNotZeroError | null
  response: AxiosResponse<T> | null
}
//后端返回结果格式处理
export interface BackendResultFormat<T = any> {
  code: number
  data: T
  rows: T
  message: string
  msg: string
}
//请求配置封装
export interface RequestConfig extends AxiosRequestConfig {
  // url接口路径
  url: NonNullable<AxiosRequestConfig['url']>
  // 接口文字描述
  desc?: string
  // 成功状态通知
  notifyWhenSuccess?: boolean
  // 完成状态通知
  notifyWhenFailure?: boolean
  // 接口限流
  limit?: number
  // url参数替换
  args?: Record<string, any>
}

/**
 * 允许定义三个泛型参数:
 *    Payload为响应数据
 *    Data为请求体参数,对应config.data
 *    Params对应URL的请求参数,对应config.params
 */
interface Request {
  <Payload = any>(config: RequestConfig): (
    requestConfig?: Partial<RequestConfig>
  ) => Promise<ResultFormat<Payload>>

  <Payload, Data>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data'>> & { data: Data }
  ) => Promise<ResultFormat<Payload>>

  <Payload, Data, Params>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) & {
        params: Params
      }
  ) => Promise<ResultFormat<Payload>>

  // 加上如果带Args泛型参数的情况,同样的,如果指定Params或Data泛型参数为undefined,则可忽略不填
  <Payload, Data, Params, Args>(config: RequestConfig): (
    requestConfig: Partial<Omit<RequestConfig, 'data' | 'params' | 'args'>> &
      (Data extends undefined ? { data?: undefined } : { data: Data }) &
      (Params extends undefined
        ? { params?: undefined }
        : { params: Params }) & {
        args: Args
      }
  ) => Promise<ResultFormat<Payload>>
}

const makeRequest: Request = <T>(config: RequestConfig) => {
  return async (requestConfig?: Partial<RequestConfig>) => {
    // 合并在service中定义的option和调用时从外部传入的option
    const mergedConfig: RequestConfig = {
      ...config,
      ...requestConfig,
      headers: {
        ...config.headers,
        ...requestConfig?.headers
      }
    }
    // 统一处理返回类型
    try {
      const response: AxiosResponse<BackendResultFormat<T>> =
        await instance.request<BackendResultFormat<T>>(mergedConfig)
      const res = response.data
      const host = window.location.hostname
      if (res.code !== 0 && res.code !== 200) {
        if (res.code == 1308) {
          window.location.assign(`http://${host}/#/login/?setting=${1}`)
        }
        const error = new CodeNotZeroError(
          res.code,
          res.message ? res.message : res.msg
        )
        return { err: error, data: res.data, response }
      }
      return { err: null, data: res.data || res.rows, response }
    } catch (err: any) {
      return { err, data: null, response: null }
    }
  }
}

export default makeRequest

3.建立interceptor文件夹,并建立如下文件,主要用于对请求和响应的业务逻辑处理

image.png

4.gateway.ts代码,主要用于在开发环境下需要有多个代理的情况下进行相关处理

import type { AxiosRequestConfig } from 'axios'
// 代码运行环境
const env: any = process.env.NODE_ENV || 'development'
// 服务的环境类
const SERVERS: Record<
  string,
  (env: string, request: { url?: string }) => string
> = {
  cpi(env, request) {
    return env == 'development' ? `${request.url}` : `${request.url}`
  },
  dpi(env, request) {
    return env == 'development' ? `${request.url}` : `${request.url}`
  }
}

export default {
  request: {
    onFulfilled: (config: AxiosRequestConfig) => {
      const path = config.url?.split('?')
      //@ts-ignore
      config.headers.Authorization = localStorage.getItem('authorization') || ''
      const TYPE = (path as string[])[0].split('/')[1]
      // TODO: /api/api
      config.url = SERVERS[TYPE]
        ? SERVERS[TYPE](env, config)
        : env == 'development'
        ? `/api/api${config.url}`
        : `/api${config.url}`
      return config
    }
  }
}

5.limiter.ts,主要用于在定义limit时执行限流逻辑

import { BackendResultFormat, RequestConfig } from '@/service/request'
import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import gateway from '@/service/interceptor/gateway'

type ResolveFn = (value: unknown) => void
const records: Record<string, { count: number; queue: ResolveFn[] }> = {}
const generateKey = (config: RequestConfig) => `${config.url}-${config.method}`

export default {
  request: {
    onFulfilled: async (config: AxiosRequestConfig) => {
      config = gateway.request.onFulfilled(config) as RequestConfig
      const { limit } = config as RequestConfig
      // 如果limit被定义,则执行限流逻辑
      if (typeof limit === 'number') {
        const key = generateKey(config as RequestConfig)
        if (!records[key]) {
          records[key] = { count: 0, queue: [] }
        }
        console.log(key, records, config, 'record1')
        const record = records[key]
        record.count += 1
        if (record.count <= limit) {
          return config
        }
        // 把该请求通过await阻塞存储在queue队列中
        await new Promise((resolve) => {
          record.queue.push(resolve)
        })
        return config
      }
      return config
    }
  },
  response: {
    onFulfilled: (response: AxiosResponse<BackendResultFormat>) => {
      const config = response.config as RequestConfig
      const { limit } = config
      if (typeof limit === 'number') {
        const key = generateKey(config)
        const record = records[key]
        console.log(key, records, response, 'record2')
        record.count -= 1
        if (record.queue.length) {
          record.queue.shift()!(null)
        }
      }
      return response
    },
    onRejected: (error: AxiosError<BackendResultFormat>) => {
      // const config = error.config
      //   const { limit } = config as RequestConfig
      //   if (typeof limit === 'number') {
      //     const key = generateKey(config)
      //     const record = records[key]
      //     record.count -= 1
      //     if (record.queue.length) {
      //       record.queue.shift()!(null)
      //     }
      //   }
      return error
    }
  }
}

6.notify.ts,主要用于请求响应业务处理

import type { AxiosError, AxiosResponse } from 'axios'
import type { BackendResultFormat, RequestConfig } from '@/service/request'

export default {
  response: {
    onFulfilled: (response: AxiosResponse<BackendResultFormat>) => {
      const { code, message, msg } = response.data
      const { desc, notifyWhenFailure, notifyWhenSuccess, method } =
        response.config as RequestConfig
      // 如果desc被定义,则执行反馈逻辑
      if (desc) {
        // 对code为0的响应做成功反馈
        if (code === 0 || code === 200) {
          if (notifyWhenSuccess !== false) {
            if (
              ['delete', 'put', 'post'].includes(
                method?.toLocaleLowerCase() || ''
              ) ||
              notifyWhenSuccess === true
            ) {
              window.$message.success(`${desc}成功`)
            }
          }
        } else if (notifyWhenFailure !== false) {
          window.$message.error(`${desc}错误~原因:${message ? message : msg}`)
        }
      }
      return response
    },
    onRejected: (error: AxiosError<BackendResultFormat>) => {
      const { response, config } = error
      // 对4xx,5xx状态码做失败反馈
      const { url, desc } = config as RequestConfig
      if (desc) {
        if (response?.status && response.statusText) {
          window.$message.error(
            `状态:${response.status}~${response.statusText}~路径:${url}~${
              response.data?.message ? response.data?.message : ''
            }`
          )
        } else {
          // 处理请求响应失败,例如网络offline,超时等做失败反馈
          window.$message.error(`原因:${error.message}~路径:${url}`)
        }
      }
      return error
    }
  }
}

7.url-args.ts文件,请求为path传参时的业务处理

import { AxiosRequestConfig } from 'axios'
import { RequestConfig } from '@/service/request'
// import { ElNotification } from 'element-plus'

export default {
  request: {
    onFulfilled: (config: AxiosRequestConfig) => {
      const { url, args } = config as RequestConfig
      // 如果args被定义,则执行路径参数替换逻辑
      if (args) {
        const lostParams: string[] = []
        const replaceUrl = url.replace(/{([^}]+)}/g, (res, arg: string) => {
          if (!args[arg]) {
            lostParams.push(arg)
          }
          return args[arg] as string
        })
        if (lostParams.length) {
          // ElNotification({
          //   type: 'error',
          //   message: `
          //               <div>
          //                   <div>内容:在args中找不到
          //                       <span>
          //                           ${lostParams.map((arg) => arg)}
          //                       </span>
          //                       属性
          //                   </div>
          //                   <div>路径:${url}</div>
          //               </div>
          //               `,
          //   dangerouslyUseHTMLString: true
          // })
          return Promise.reject(new Error('在args中找不到对应的路径参数'))
        }
        return { ...config, url: replaceUrl }
      }
      return config
    }
  }
}

使用示例:

export const apiOpenNetworkConfig = makeRequest({
  url: '/base/network/open/{id}',
  method: 'post',
  desc: '启用配置',
  notifyWhenSuccess: true,
  notifyWhenFailure: true
})

至此所有的代码粘贴完毕了,也是第一次在项目中遇到有这块封装处理在,因此总结到此篇文章中,如果有任何疑问和补充欢迎大家在评论补充