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

点击在线阅读,体验更好 链接
现代JavaScript高级小册 链接
深入浅出Dart 链接
现代TypeScript高级小册 链接
linwu的算法笔记📒 链接

引言

在前面两篇文章中得到反馈效果不错,今天继续出第三篇,以二次封装axios为例

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

需求

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

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

请求方法的统一封装

ts 复制代码
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文件:

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

实现:

ts 复制代码
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)。
ts 复制代码
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)
})

实现

ts 复制代码
  
   /**
   * 给个请求
   * 配置正确的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,比如后端还未实现,已有的接口走线上数据,这样以配置,就简单明了多了

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

支持restful风格

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

实现:

ts 复制代码
  /**
   * 替换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 取消请求:

ts 复制代码
// 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]
  }
}

支持重新请求

ts 复制代码
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
}

支持缓存

ts 复制代码
// 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
  }
})

支持限流

ts 复制代码
// 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包

小册

整理两本小册,一本是前端面试小册,一本是图解算法,阅读体验都很好,欢迎添加我微信linwu-hi获取

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom9 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github