前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于Axios二次封装请求库,带你重构面试亮点

基于Axios二次封装请求库,带你重构面试亮点

作者头像
linwu
发布2023-08-14 08:11:46
2660
发布2023-08-14 08:11:46
举报
文章被收录于专栏:编程时光编程时光

在我以往的面试中,听到候选人最多的就是项目中二次封装axios,但是当真正深入挖掘,往往得不到有用的信息。 那么面试官是想听到什么样的亮点呢?这篇文章我们重点分析一下,并且也可以封装自己的axios请求库。

需求

在项目中,我们可能存在这些痛点:

  • 接口统一管理
  • 支持多host问题
  • 支持区分env
  • 支持restful风格
  • 支持取消请求
  • 支持接口错误重试
  • 支持缓存
  • 支持限流

请求方法的统一封装

代码语言:javascript
复制
export class  Apis{

  public common: RequestOptions;
  // 默认的server配置
  public base!: string;
  // server服务的集合
  public serverMap: ServerMap;
  // 对象形式的请求方法集合
  public apiMap: ApisMap;
  // 挂载所有请求方法的集合对象
  public apis: ApisInstance;
  // axios实例化对象
  public instance: AxiosInstance;

  constructor(common?: RequestOptions, serverMap?: ServerMap, apiMap?: ApisMap) {
  
  }

  public get<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    request = { ...request, method: 'GET' };
    return this.request(url, request);
  }

  public delete<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    request = { ...request, method: 'DELETE' };
    return this.request(url, request);
  }

  public post<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    request = { ...request, method: 'POST' };
    return this.request(url, request);
  }

  public put<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    request = { ...request, method: 'PUT' };
    return this.request(url, request);
  }

  public patch<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    request = { ...request, method: 'PATCH' };
    return this.request(url, request);
  }

  public request<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
    const rest = request.rest || {};
    let path = url;
    if (Object.keys(rest).length) {
      path = this.restful(url, rest);
    }
    // 合并公共配置
    const options = { ...this.common, ...request };
    return this.instance.request({
      ...options,
      url: path,
    });
  }
}

接口统一管理

在项目中,实际每个请求的写法都是一样的,开发过程中,我不想在每个页面都重复写请求方法,我想通过JSON配置的方式做接口统一管理,比如:

在Home Module下,新建apis.ts文件:

代码语言:javascript
复制
export default {
  getBaseInfo: {
    method: 'get',
    url: '/base/get'
  },
  getBaseRestInfo: {
    method: 'get',
    url: '/base/info'
  }
}

实现:

代码语言:javascript
复制
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import {
  ApisMap,
  ServerMap,
  ApisInstance,
  ApisConfig,
  ResolvedFn,
  RejectedFn,
  Middleware,
  Rest
} from './types'

class Apis {
  base: string
  serverMap: ServerMap
  apiMap: ApisMap
  instance: ApisInstance
  axiosInstance: AxiosInstance
  constructor(serverMap: ServerMap, apiMap: ApisMap, common?: AxiosRequestConfig) {
    /**
     * 支持公共配置
     */
    this.axiosInstance = axios.create(common)
    this.serverMap = serverMap
    this.apiMap = apiMap
    this.instance = {}
    this.base = this.getDefault()
    this.combine2Request()
  }
  /**
   * 获取默认的配置
   */
  getDefault(): string {
    let base = ''
    for (const key of Object.keys(this.serverMap)) {
      /**
       * 找到默认的配置值
       */
      if (this.serverMap[key].default) {
        base = key
      }
    }
    if (!base) {
      console.error('apis: 找不到默认服务器配置')
    }
    return base
  }
  

  combine2Request(): void {
    for (const key of Object.keys(this.apiMap)) {
      this.instance[key] = (config?: ApisConfig) => {
        let result: ApisConfig = this.apiMap[key]
        if (config) {
          result = this.rest2Combine(this.apiMap[key], config)
        }
        return this.axiosInstance.request(result)
      }
    }
  }
}


export default createInstance

支持多host

应用可能需要跟多个服务交互,这时候涉及多host,我们想统一处理:

  • 定义serverMap
  • serverMap对象定义了两个服务:baseServerapi-test
  • 对于每个服务,都提供了一个baseMap对象来描述不同环境下的base URLs。
  • baseServer有一个default属性设置为true,表示它可能是默认选择的服务器。
  • 定义apiMap
    • apiMap对象定义了两个API:getBaseInfogetBaseRestInfo
    • 每个API都有一个HTTP方法(method)和一个URL路径(url)。
代码语言:javascript
复制
import createInstance from 'apis'
import { ApisMap } from 'apis/types'

const serverMap = {
  baseServer: {
    baseMap: {
      localprod: '',
      prod: 'https://wwww.baidu.com',
      stage: 'https://wwww.baidu.com',
      test: 'https://wwww.baidu.com',
      dev: 'https:/wwww.baidu.com',
      local: 'http://127.0.0.1:4320',
      baseURL: 'https://localhost:8080'
    },
    default: true
  },
  'api-test': {
    baseMap: {
      localprod: '',
      prod: 'https://www.baidu.com',
      stage: 'https://www.baidu.com',
      test: 'https://www.baidu.com',
      dev: 'https:/www.baidu.com',
      local: `http://127.0.0.1:4320`,
      baseURL: 'https://localhost:8080'
    }
  }
}

const apiMap: ApisMap = {
  getBaseInfo: {
    method: 'get',
    url: '/base/get'
  },
  getBaseRestInfo: {
    method: 'get',
    url: '/base/get/:id/kill/:test'
  }
}

let apis = createInstance(serverMap, apiMap)

apis.getBaseInfo({ params: { name: 'linwu' } }).then(res => {
  console.log(res)
})

实现

代码语言:javascript
复制
   /**
   * 给个请求
   * 配置正确的baseURL
   * 如果没有baseURL就读默认的
   */
  formatConfigUrl(): void {
    for (const key of Object.keys(this.apiMap)) {
      const item = this.apiMap[key]
      if (!item.server) {
        item.server = this.base
      }
      this.apiMap[key] = { ...this.serverMap[item.server], ...item }
    }
  }

支持区分env

这样配置有个好处。有时候我们新增的接口想走本地mock,比如后端还未实现,已有的接口走线上数据,这样以配置,就简单明了多了

代码语言:javascript
复制
baseMap: {
      prod: 'https://wwww.baidu.com',
      test: 'https://wwww.baidu.com',
      local: 'http://127.0.0.1:4320',
      baseURL: 'https://localhost:8080'
    },
代码语言:javascript
复制
export default {
  getBaseInfo: {
    method: 'get',
    url: '/base/get',
    env:'local'
  },
  getBaseRestInfo: {
    method: 'get',
    url: '/base/get/:id/kill/:test',
    env:'test'
  }
}

支持restful风格

代码语言:javascript
复制
export default {
  getBaseInfo: {
    method: 'get',
    url: '/base/get',
    env:'local'
  },
  getBaseRestInfo: {
    method: 'get',
    url: '/base/get/:id/qs/:test',
    env:'test'
  }
}

实现:

代码语言:javascript
复制
  /**
   * 替换restful请求中的url
   */
  restful(url: string, rest: Rest): string {
    /**
     * [xyz]一个字符集合。匹配方括号中的任意字符
     * 比如正则表达式是[abcd]==>匹配brisket"中的‘b’
     */
    const regex = /\:[^/]*/g
    /**
     * 一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。
     * 另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用。
     * 匹配模式是这样的\:[^/]* 为一个整体 全局g下多次匹配 也就是多次调用fn
     * [^/]匹配得到的是一个字符 只要匹配的url出现的一个字符在 [^/]中出现就匹配成功 但是是单个的 所以要多次匹配
     */

    return url.replace(regex, p => {
      console.log(p)
      /**
       * :id ===>返回id
       */
      const key = p.slice(1)
      if (rest[key]) {
        return rest[key]
      }
      return p
    })
  }

支持取消请求

从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求:

代码语言:javascript
复制
// 1. 支持取消请求
const cancelMap: { [key: string]: any } = {}

Apis.reqMiddleware.push({
  onFulfilled: (config) => {
    const source = axios.CancelToken.source()
    config.cancelToken = source.token
    cancelMap[config.url!] = source
    return config
  }
})

createInstance.cancel = function(url: string) {
  if (cancelMap[url]) {
    cancelMap[url].cancel('Request was cancelled')
    delete cancelMap[url]
  }
}

支持重新请求

代码语言:javascript
复制
function createInstance(
  serverMap: ServerMap,
  apiMap: ApisMap,
  common?: AxiosRequestConfig
): ApisInstance {
  Apis.resMiddleware.push({
    onFulfilled: void 0,
    onRejected: function axiosRetryInterceptor(err) {
      const config = err.config
      /**
       * 如果没有retry配置那么就不走这个拦截器
       * 因为发生错误,我们还在这个拦截器中 request2 interceptor --> request1 interceptor-->dispatchRequest--> response1 interceptor--> response2 interceptor
       * 这时候在拦截器中重新发起请求把得到的响应结果发给最后的Promise
       * 最后的Promise注册中我们成功和失败的业务
       * 这样可以避免:其他的那个几十个.vue页面的 this.$axios的get 和post 的方法根本就不需要去修改它们的代码。
       */
      if (!config || !config.retry) return Promise.reject(err)
      /**
       * 已经尝试retry的次数
       */
      config.__retryCount = config.__retryCount || 0

      if (config.__retryCount >= config.retry) {
        return Promise.reject(err)
      }

      config.__retryCount += 1
      /**
       * 等待多少秒后才进行retry
       */
      const backoff = new Promise(function(resolve) {
        setTimeout(function() {
          resolve()
        }, config.retryDelay || 1)
      })
      return backoff.then(function() {
        /**
         * 返回结果
         * 是个Promise对象
         * 不reject 返回的数据被成功的回调拿到
         */
        return axios(config)
      })
    }
  })
  const apis = new Apis(serverMap, apiMap, common)
  /**
   * new过后清空以前的拦截器队列
   * 因为new完一个实例过后,拦截器信息可以作废
   * 所以要确保你实例化之前先注册拦截器
   */
  Apis.reqMiddleware = []
  Apis.resMiddleware = []
  return apis.instance
}

支持缓存

代码语言:javascript
复制
// 3. 支持接口缓存
const cacheMap: { [key: string]: any } = {}

Apis.resMiddleware.push({
  onFulfilled: (response) => {
    cacheMap[response.config.url!] = response
    return response
  }
})

Apis.reqMiddleware.push({
  onFulfilled: (config) => {
    if (cacheMap[config.url!]) {
      throw new axios.Cancel('Request was cached')
    }
    return config
  }
})

支持限流

代码语言:javascript
复制
// 2. 支持接口限流
const LIMIT = 5 // 例如,5个请求每秒
const INTERVAL = 1000 // 1秒
let tokens = LIMIT

setInterval(() => { tokens = LIMIT }, INTERVAL)

Apis.reqMiddleware.push({
  onFulfilled: (config) => {
    if (tokens > 0) {
      tokens--
      return config
    } else {
      throw new Error('Rate limit exceeded')
    }
  }
})

总结

上述的apis是我实际项目中封装的,当然文章给的大部分是伪代码,但是思路是对的,大家可以按照这个思路封装自己项目中的请求库,然后发布成npm包

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-08-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需求
  • 请求方法的统一封装
  • 接口统一管理
  • 支持多host
  • 支持区分env
  • 支持restful风格
  • 支持取消请求
  • 支持重新请求
  • 支持缓存
  • 支持限流
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档