手把手带你封装一次 Axios

835 阅读11分钟

每次接手的项目都是别人封装好的东西,时间久了,我们也就只是一个代码搬运工,工作五年,依旧不知道axios 是咋封装的,想想都挺对不起自己的,难道真的35岁以后,只能送外卖了吗? 为了35岁以后,还能有口饭吃,自己造的轮子,还是自己造起来吧,看别人代码挺难的,但是我就喜欢从最简单入手,一次性搞定Axios的封装。

本人测试案例不依赖任何框架,就是简单的html,js,css,目的是为了要大家从根本上认识最原始的js。为什么这么做呢,原因是我在面试过程中发现很多应聘者,抛开框架就不会搞js了,它对整个前端的认识是狭隘的,片面的,或许通过背面试题他们都能答出一些皮毛,但是一旦深入,就会死翘翘。对问题的深入理解决定了咱们的工资高低是不是?再者,如果你无法理解最原始的html,js, css,那么你如何能够全面掌握vue,react呢?

上传下载封装参看上一篇# 二次封装 Axios 下载文件和上传文件

Axios封装三部曲:

  1. 创建axios实例
  2. 拦截请求
  3. 拦截响应
  4. 导出方法

基础代码如下:

不依赖框架,看看最原始的html和js咋用axios,在此基础上我相信,你会对axios及其他相关知识有新的认识,毕竟框架下的东西,把很多难点都遮盖了。

创建一个项目:axios-test

cd axios-test

npm init -y

npm i axios -S

创建几个文件,如下:

image.png

为啥要启动服务呢,就是要前端开发者自己看看,从接口到前端,其实没有什么奥秘。自己动手丰衣足食呗!

1.服务端:

image.png

2.html

image.png

3.index.js

image.png

4.axios.js

image.png

测试

1.启动服务执行: node ./server/index.js,如果你没有安装express, 你就安装下,然后再启动。

2.启动前端有多种方法:

  1.   如果你集成了 vite 就直接执行 vite 命令就好了
  2.   如果没有集成任何打包工具,那就直接打开 html 文件,右键点击 open with live server ,如果没有这个选项大概是因为你的 VScode 没有安装 live-server 插件,安装下就好了,具体如图所示:
  3.   安装下 http-server 执行命令: npx http-server -p 5666
  

image.png

测试结果如下:

image.png

axios的代码如下:use() 方法的入参是两个函数,一个处理拦截成功,一个处理拦截失败。成功了,就把接受到的,处理后的配置项return出去,失败了就把错误用promise包裹下return出去。

import axios from '../../node_modules/axios/dist/esm/axios.js'; //直接用import axios from 'axios' 浏览器无法解析,需要esmodule模块

// 创建 axios 实例
const instance = axios.create({
    baseURL: ' http://127.0.0.1:3002/api',
    timeout: 10000,
    timeoutErrorMessage: '请求超时,请稍后重试',
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    }
  })
  
  // 请求拦截器
  const requestConfig = (config) => {
    return config;
  } //所有请求拦截的东西全部放这里
  instance.interceptors.request.use(requestConfig, (error) => Promise.reject(error))
  
  // 响应拦截器
  const responseData = (data) => {
    return data;
  } //所有响应拦截的东西全部放这里
  instance.interceptors.response.use(responseData, (error) => Promise.reject(error))
  
  export default {
    get(url,params,options,){
      return instance.get(url, { params, ...options })
    },
    post(url, params, options){
      return instance.post(url, params, options)
    }
  }

接下来,我们做的所有的事情都是在 axios.js 里面扩展。 比如:在axios里面添加防抖,节流,loading,错误展示,跳转页面,token验证,文件的上传,下载等等!上一篇文章( 二次封装 Axios 下载文件和上传文件)已经对文件的上传和下载封装做了全面的解释,本篇文件不再解释。

1.错误拦截

咱们在请求的时候,错误状态码会有很多,最常见的就是500和404,他们每一个都代表不一样的意思,所以需要我们从接受到的数据里面,把他们分离出来,给出明确的提示。以免每个接口报错,都要处理一次,麻烦!

image.png

image.png

调试记得用VScode的调试,不要只会console.log了,还有记得修改服务端接口:

image.png

带着大家调试下看看,点击vscode的debugger模式,创建launch.json文件,然后将端口改成前端页面的端口,如下:(如果你不会,请移步《# 面试官问:在日常的开发中,你是如何调试代码的?》

image.png

点击绿色箭头就开始进入debugger模式,我们启动下看看

image.png

debugger调试写代码明显比console.log的速度快很多。这样错误的封装就结束了。

2.防抖

防抖的基础是啥?就是当我执行完这一次以后,要等一下,等多少秒之后再次执行。表现为点击多次,只执行一次呗!写个简单的防抖:把这个html运行起来,就能用了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">点我</button>
    <script>
        //节流每次点击一次就把上次的timer删掉
        function myDebounce(fn, delay){
            let timer;

            return function(){
                if(timer){
                clearTimeout(timer)
                }

                timer = setTimeout(()=>{
                    fn.apply(this, arguments)
                }, delay)
            }
        }

        document.getElementById('btn').addEventListener('click',myDebounce(()=>{
            console.log(222)
        },1000))
    </script>
</body>
</html>

首先我们思考下防抖的应用场景,一般在搜索的时候会用到,不是所有的接口都要防抖的,所以没有必要给所有的接口加防抖,需要防抖的就传个防抖参数进来,不传就不防抖。所以说,在发送请求的时候应该有是参数来控制下,我们根据参数来确定到底要不要进入防抖控制。

image.png

引入axios

image.png

请求下

image.png

测试下看看:

多次点击按钮,只生效一次,600ms以后再次生效。

image.png

思考下为什么防抖的时候要用Map保存下时间,而不是直接像下图:

image.png

每次请求都是一个axios的实例,如果用timer的话,所有的请求公用一个timer,明显就不对了,所以要单独存放; 测试如下:我再html里面放了2个按钮请求接口

image.png

结论如下:btn2直接被拦截了,一次都没有触发。

image.png

用map的长这样,谁是谁的,互不干涉。

image.png

3.节流

手写节流的基础代码如下:自己启动个服务跑一下看看。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div style="height: 1000px"></div>
    <script>
        // 固定时间触发一次
        function myThrottle(fn, time){
            let startTime = 0
            return function(){
                let endTime = new Date();

                if(endTime-startTime > time){
                    fn.apply(this, arguments);
                    startTime = endTime
                } 
            }
        }
        
        function myScroll(){
            console.log(1111)
        }
        document.addEventListener('scroll',myThrottle(myScroll,1000))
    </script>
</body>
</html>

接下咱们可以把节流封装到自己的axios里面去,只要有isThrottle为true,那axios请求自动开始节流,否则不节流。

image.png

image.png

image.png

测试如下:

image.png

给所有的接口添加token等等,都可以用这样的方法写。我把他们全部放在不同的文件里面,是为了好理解,我看很多项目里面,大家都喜欢把axios的封装放在一个文件里面,导致我们在业务开发的时候,不想花费任何时间去阅读它。

如果你想快速测试,用这个模板

import axios from 'axios'; //直接用import axios from 'axios' 浏览器无法解析,需要esmodule模块

// 创建 axios 实例
const instance = axios.create({
    baseURL: ' http://127.0.0.1:3002/api',
    timeout: 10000,
    timeoutErrorMessage: '请求超时,请稍后重试',
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    }
  })
  
 // 防抖
const cancelMap = new Map()
instance.interceptors.request.use(config => {
  const tokenKey = `${config.method}-${config.url}`
  const cancel = cancelMap.get(tokenKey)
  if (cancel) {
    cancel()
  }
  return new Promise(resolve => {
    const timer = setTimeout(() => {
      clearTimeout(timer)
      resolve(config)
    }, 800)
    cancelMap.set(tokenKey, () => {
      clearTimeout(timer)
      resolve('节流处理中,请稍后重试!')
    })
  })
}, error => {
  console.log(error)
  return Promise.reject(error)
})

// 节流
let lastTime = new Date().getTime()
instance.interceptors.request.use(config => {
//两种写法
//   const nowTime = new Date().getTime()
//   if (nowTime - lastTime < 600) {
//     return Promise.reject('节流处理中,请稍后重试!')
//   }
//   lastTime = nowTime
//   return config
    const time = config?.time || 600;
    return new Promise((resolve, reject) => {
        const nowTime = new Date().getTime()
        if (nowTime - lastTime < time) {
            reject('节流处理中,稍后再试!')
        }
        
        lastTime = nowTime
        resolve(config)
    })
}, error => {
  console.log(error)
  return Promise.reject(error)
})

// 响应
instance.interceptors.response.use(response => {
    return response
  }, error => {
    console.log(error)
    return Promise.reject(error)
  })
  
export default {
  get(url,params,options,){
    return instance.get(url, { params, ...options })
  },
  post(url, params, options){
    return instance.post(url, params, options)
  }
}

是不是很简单呀!

4.防止接口重复请求

防止接口重复请求的办法有哪些呢?

  • 1.全屏loading等待
  • 2.收集请求信息,强制拦截
  • 3.缓存请求信息派发共享

挂起请求的办法就是:return Promise.reject()

1.1 全屏loading等待

image.png

这个方案固然已经可以满足我们目前的需求,直接搞个全屏Loading不太美观,何况有些组件本身就自带局部loading,两个圈圈一起转?好玩吗?会挨骂的呀!

1.2 收集请求信息,拦截

请求内容包括请求方法地址参数以及请求发出的页面hash。我们可以把这些信息转成一个字符串,然后存放在set里面。接下来就在请求拦截和响应拦截里面操作这个set。

image.png

axios拿到请求以后,先去set里面对比,如果不存在就加入,存在报错,拦截掉。等set里面的请求拿到数据以后,就把set里面对应的key删掉。

image.png

这个方式感觉很不错,但是如果一个页面2个组件,同时调用同一个接口呢?还有,如果我们的接口做了单独的异常处理呢?它就会出现明明没有调接口,却报错了!隐藏的bug还真不少!

1.3 收集请求信息,拦截

延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以不直接把请求挂掉,而是对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求

image.png

实现要点:

  • 挂起的请求时,我们要用到发布订阅模式
  • 对于挂起的请求,我们需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过return Promise.reject()来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理
import axios from "axios"
 
let instance = axios.create({
    baseURL: "/api/"
})
 
// 发布订阅
class EventEmitter {
    constructor() {
        this.event = {}
    }
    on(type, cbres, cbrej) {
        if (!this.event[type]) {
            this.event[type] = [[cbres, cbrej]]
        } else {
            this.event[type].push([cbres, cbrej])
        }
    }
 
    emit(type, res, ansType) {
        if (!this.event[type]) return
        else {
            this.event[type].forEach(cbArr => {
                if(ansType === 'resolve') {
                    cbArr[0](res)
                }else{
                    cbArr[1](res)
                }
            });
        }
    }
}
 
 
// 根据请求生成对应的key
function generateReqKey(config, hash) {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
 
// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()
 
// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
    let hash = location.hash
    // 生成请求Key
    let reqKey = generateReqKey(config, hash)
    
    if(pendingRequest.has(reqKey)) {
        // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
        // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
        let res = null
        try {
            // 接口成功响应
          res = await new Promise((resolve, reject) => {
                    ev.on(reqKey, resolve, reject)
                })
          return Promise.reject({
                    type: 'limiteResSuccess',
                    val: res
                })
        }catch(limitFunErr) {
            // 接口报错
            return Promise.reject({
                        type: 'limiteResError',
                        val: limitFunErr
                    })
        }
    }else{
        // 将请求的key保存在config
        config.pendKey = reqKey
        pendingRequest.add(reqKey)
    }
 
    return config;
  }, function (error) {
    return Promise.reject(error);
  });
 
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response)
    return response;
  }, function (error) {
    return handleErrorResponse_limit(error)
  });
 
// 接口响应成功
function handleSuccessResponse_limit(response) {
      const reqKey = response.config.pendKey
    if(pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(response))
      }catch(e) {
        x = response
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'resolve')
      delete ev.reqKey
    }
}
 
// 接口走失败响应
function handleErrorResponse_limit(error) {
    if(error.type && error.type === 'limiteResSuccess') {
      return Promise.resolve(error.val)
    }else if(error.type && error.type === 'limiteResError') {
      return Promise.reject(error.val);
    }else{
      const reqKey = error.config.pendKey
      if(pendingRequest.has(reqKey)) {
        let x = null
        try {
          x = JSON.parse(JSON.stringify(error))
        }catch(e) {
          x = error
        }
        pendingRequest.delete(reqKey)
        ev.emit(reqKey, x, 'reject')
        delete ev.reqKey
      }
    }
      return Promise.reject(error);
}
 
export default instance;

这个拦截也有个问题,就是对上传文件来说,他们永远调用的都是一个接口,我的总不能每次都拦截吧,所以需要用请求的FormData类型去刷选他。

function isFileUploadApi(config) {
  return Object.prototype.toString.call(config.data) === "[object FormData]"
}

总结下:

1.利用 axios.create() 工厂方法创建一个 asxios 实例。

2.每个 axios 实例都有一个拦截器,每个拦截器都有两个对象,一个是request,专门用来拦截请求的。另一个是 response 专门用来拦截响应。他们都有一个use方法,用来处理拦截任务。

3.在 request 的拦截器里面有个重要的参数是config,从它里面可以拿到发起请求时候的所有判断条件。然后在这里修改请求头,防抖,节流,判断token是不是失效,文件的上传下载等等

4.在 response 的拦截器里面有个重要的参数是data,从它里面可以拿到后端返回的所有值。然后在这里你可以将解构后端传过来的数据,对请求错误做综合处理。

5.最后一步就是返回 axios 实例。

整个封装就上面这五个步骤。

推荐文章:

blog.csdn.net/weixin_4908…