完整的Axios封装-单独API管理层、参数序列化、取消重复请求、Loading、状态码...

71,335 阅读10分钟

前言

Axios 相信对Vue熟悉的铁汁对它不会感到陌生了(当然不熟悉Vue你也可以认识它),这简直就是前端近年来的一大杀器,自从Vue2开始之后,官方推荐使用axios来进行网络请求,后面基本大部分Vue项目都能瞧见它的身影。

接下来我们就话不多说了,直接开始今天的主题,虽然axios很强,但是单纯的axios并不能满足我们日常的使用,因此很多时候我们都需要对axios进行二次封装,接下来我们就来详细讨论讨论。

准备工作

前端

Vite 一个超级超级超级快的开发神器,依旧是 尤雨溪 大大的杰作,这次就用vite来初始化项目来完成编码。

直接初始化项目,更多详情

npm init @vitejs/app

下载axios依赖。

npm install axios

后端

借用node自个搭建一个简单的服务器,之所以自己弄个服务,不随便网上找个接口请求,也是为了后面方便验证一些特殊情况,比如请求超时、不同HTTP状态码、各种响应的数据结构等等。

在上面初始化好的项目目录下直接创建 service 目录,搭建服务的详情可以点这里(代码很简单,直接复制下面的router.js文件与app.js文件的代码即可)

image.png

之后我们通过命令 node app.js 启动服务,就能拥有三个这样子的接口:

独立的API管理层

做好以上准备工作后,我们就可以开始进入正题了。一个项目的所有API接口统一管理是非常重要的,这样便于后期的更新维护,为此我们单独划分出API层来管理项目的所有API,以模块来划分每个API归属的文件。

我们在项目中创建 api文件夹 用来管理所有的API,创建 axios.js 文件二次封装axios,其他文件就是对应项目中的功能模块,如所有商品相关的API就放在 goods.js 文件,所有订单相关的API就放在 order.js 中,这样子就很有条理性。

image.png

一、我们先来简单的编写 axios.js

import axios from 'axios';

function myAxios(axiosConfig) {
  const service = axios.create({
    baseURL: 'http://localhost:8888', // 设置统一的请求前缀
    timeout: 10000, // 设置统一的超时时长
  });

  return service(axiosConfig)
}

export default myAxios;

需要注意的是 service(axiosConfig) 返回的是一个Promise对象哦。
(上面之所以设计成一个函数是为了后续的一些封装操作,之后会讲到)

二、下面我们来 goods.js 中编写获取商品列表的API。

import myAxios from './axios';

export function getListAPI(paramsList) {
  return myAxios({
    url: '/api/list',
    method: 'get',
  })
}

如果请求是绝对路径,也可以直接填入url参数中,baseUrl 参数不并会再加上个前缀,这是baseUrl参数的特性哦。

三、最后我们在页面中具体使用,在 App.vue 文件中随便加一个按钮,点击触发请求。

<template>
  <button @click="getList">点击</button>
</template>

<script lang='ts'>
import {defineComponent} from 'vue'
import {getListAPI} from '@/api/goods.js';
export default defineComponent({
  setup() { 
    function getList() {
      getListAPI().then(res => {
        console.log(res)
      })
    }

    return {
      getList
    }
  }
})
</script>

到此,我们就简单的划分出 API 管理层了,这样我们每次新增加一个 API,只需要找到对应模块的 API 文件去添加即可,然后再到具体页面导入使用就行啦。

你可以用 xxxAPI 结尾的形式来命名 API 方法,这样子可以和普通方法区别出来,既明确又不用再为取名而烦恼了,可谓一举两得呢。(^ω^)

可能很多小伙伴会觉得这样子每次都需要去导入,会很麻烦,现在网上有很多做法就是直接将所有的 API 都挂载在 Vue 的实例上,直接通过 this.$axios.getList() 这样子去使用。个人感觉这样确实挺方便的,但是,它比较适合项目小、API 比较少的情况,要是项目比较庞大,里面的 API 比较多,就容易开始混淆,不好分类 API,使用 this 也还要考虑 this 指向问题,Vue3 更是没有所谓的 this

POST请求参数序列化

在POST请求中的 Content-Type 常见的有以下3种形式:

  • Content-Type: application/json
  • Content-Type: application/x-www-form-urlencoded
  • Content-Type: multipart/form-data

现在主流基本在用application/json形式,Axios默认以这种形式工作,我们给后端接口传递参数也简单,直接丢在其data参数就行了。
我们 user.js 文件中编写登录API

import myAxios from './axios';

export function login(paramsList) {
  return myAxios({
    url: '/api/login',
    method: 'post',
    data: paramsList
  });
}

在具体页面导入调用该方法传递相关参数即可。

但是有时候后端要求Content-Type必须以application/x-www-form-urlencoded形式,那么通过上面传递的参数,后端是收不到的,我们必须对参数数据进行所谓的序列化处理才行,让它以普通表单形式(键值对)发送到后端,而不是json形式,更多关于序列化内容就自行百度啦,这里就告诉你如何做就行啦。

// user.js
import myAxios from './axios';

export function loginAPI(paramsList) {
  return myAxios({
    url: '/api/login',
    method: 'post',
    data: paramsList,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    transformRequest: [
      (data) => {
        let result = ''
        for (let key in data) {
          result += encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) + '&'
        }
        return result.slice(0, result.length - 1)
      }
    ],
  });
}

我通过 headers 来指定Content-Type的形式,对于 transformRequest 就是允许在向服务器发送前,修改请求数据,但只能用在 'PUT','POST' 和 'PATCH' 这几个请求方法,且后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream,更多的还有 transformResponse 能在传递给 then/catch 前,允许修改响应数据,其余更多参数的可以去 Axios文档 查看。

(开始把Axios二次封装设计成一个函数,这里就很方便能单独配置单个请求不同的axios配置了,是不是很棒,但真正的方便是在后面自定义Loading的时候才更方便点哦,接着往下看咯)

最后通过浏览器network点击图中红框中的 view source就能看到序列化后的参数形式了。

image.png

image.png

用qs模块来序列化参数

我们也能通过第三方依赖来序列化参数,就更加方便简洁,下载qs模块。

npm install qs
// user.js
import qs from 'qs';
export function loginAPI(paramsList) {
  return myAxios({
    url: '/api/login',
    method: 'post',
    data: paramsList,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    transformRequest: [
      (data) => {
        return qs.stringify(data)
      }
    ],
  });
}

取消重复请求

说起这个重复请求,感觉用到得比较少,心理总有怎么一种想法就算多请求一次又能怎么样,服务器会塌吗?页面会挂吗?明显不会嘛,不要大惊小怪,哈哈哈。再说没事怎么会多发重复的请求呢?不可能的。

image.png

而且做取消重复请求操作,其实取消后的请求还是有可能会到达了后端,只是前端浏览器不处理而已,但是呢,哎,我们还是得做做工作,不,非做不可,所谓以防万一,严谨,程序猿需要严谨!!!

发生重复请求的场景一般有这两个:

  • 快速连续点击一个按钮,如果这个按钮未进行控制,就会发出重复请求,假设该请求是生成订单,那么就有产生两张订单了,这是件可怕的事情。当然一般前端会对这个按钮进行状态处理控制,后端也会有一些幂等控制处理策略啥的,这是个假设场景,但也可能会发生的场景。
  • 对于列表数据,可能有tab状态栏的频繁切换查询,如果请求响应很慢,也会产生重复请求。当然现在很多列表都会做缓存,如Vue中用 <keep-alive />

如何取消一个已发送的请求

在开始正题前,我们要先来了解一下,如何取消一个已发送的请求,不知道铁汁们对JS中的 XMLHttpRequest 对象是否了解?(不知道也当你知道了) 你只要知道axios底层就是依赖于它的就行,也就是它的二次封装,那我们对axios再次封装,也就是三次封装?套娃?

XMLHttpRequest 对象是我们发起一个网络请求的根本,在它底下有怎么一个方法 .abort(),就是中断一个已被发出的请求。

image.png

那么axios自然也有对其的相关封装,就是 CancelToken文档上介绍的用法:

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

简单理解就是通过 new axios.CancelToken()给每个请求带上一个专属的CancelToken,之后会接收到一个cancel() 取消方法,用于后续的取消动作,所以我们需要对应的存储好这个方法。

开始正题

通过上面的了解,下面就能进入正题部分了,接下来我们大致整体思路就是收集正在请求中接口,也就是接口状态还是pending状态的,让他们形成队列储存起来。如果相同接口再次被触发,则直接取消正在请求中的接口并从队列中删除,再重新发起请求并储存进队列中;如果接口返回结果,就从队列中删除,以此过程来操作。

判断重复请求并储存进队列

首先我们要收集请求中的接口并判断哪些请求是重复请求,我们才能取消它,那么如何判断呢?很简单,只要是请求地址、请求方式、请求参数一样,那么我们就能认为是一样的。而我们要存储的队列里面的数据结构很明显应该是以键值对的形式来存储,这里面我们选择 Map 对象来操作。

// axios.js
const pendingMap = new Map();

/**
 * 生成每个请求唯一的键
 * @param {*} config 
 * @returns string
 */
function getPendingKey(config) {
  let {url, method, params, data} = config;
  if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象
  return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');
}

/**
 * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
 * @param {*} config 
 */
function addPending(config) {
  const pendingKey = getPendingKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingMap.has(pendingKey)) {
      pendingMap.set(pendingKey, cancel);
    }
  });
}

取消重复请求并出删除队列

// axios.js
/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config) {
  const pendingKey = getPendingKey(config);
  if (pendingMap.has(pendingKey)) {
     const cancelToken = pendingMap.get(pendingKey);
     cancelToken(pendingKey);
     pendingMap.delete(pendingKey);
  }
}

添加拦截器

// axios.js
function myAxios(axiosConfig) {
  const service = axios.create({
    baseURL: 'http://localhost:8888', // 设置统一的请求前缀
    timeout: 10000, // 设置统一的超时时长
  });

  service.interceptors.request.use(
    config => {
      removePending(config);
      addPending(config);
      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );

  service.interceptors.response.use(
    response => {
      removePending(response.config);
      return response;
    },
    error => {
      error.config && removePending(error.config);
      return Promise.reject(error);
    }
  );

  return service(axiosConfig)
}

我们在上面提到的 getList() 方法里面简单模拟连续发出了三次重复请求,然后把浏览器设置成3G模式,就能看到效果啦。

// App.vue
function getList() {
  getListAPI().then(res => {console.log(res)})
  setTimeout(() => {
    getListAPI().then(res => {console.log(res)})
  }, 200);
  setTimeout(() => {
    getListAPI().then(res => {console.log(res)})
  }, 400);
}

image.png

需要注意,上面说了取消正在请求中的接口,说明这接口有可能已经到达后端了,只是后端响应慢,所以如果你的接口响应比较快的话,就很难看到效果;如果你是自己搭建的服务,只要通过接口返回时延时下就可以看到效果;又或者通过浏览器的network调整网络速度也可以哦。image.png

对于取消后的请求我们也应该有个合理的处理,不能就不管了,尽可能的达到代码可控的底部,它会被归类到异常里面,下面会说到(^ω^)。

配置化

之所以弄成配置化取消重复请求,是因为可能存在一些特殊变态的场景情况,是需要重复请求,如输入实时搜索、实时更新数据等,反正就是可能存在吧。( ̄y▽ ̄)~*

// axios.js
function myAxios(axiosConfig, customOptions) {
  const service = axios.create({
    baseURL: 'http://localhost:8888', // 设置统一的请求前缀
    timeout: 10000, // 设置统一的超时时长
  });

  // 自定义配置
  let custom_options = Object.assign({
    repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
  }, customOptions);

  service.interceptors.request.use(
    config => {
      removePending(config);
      custom_options.repeat_request_cancel && addPending(config);
      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );
  ...
}

我们在上面增加了一个自定义配置的参数,现在每个API方法就能拥有两个参数,第一个参数传递的是axios原本的一些配置,第二个参数就是我们自己的一些自定义参数了,如我们定义 repeat_request_cancel 来控制是否开启取消重复请求的功能。后续更多功能,我们也能添加进其中,相当于可定制化每个API方法,是不是很棒!!!

// goods.js
export function getListAPI(paramsList) {
  return myAxios({
    url: '/api/list',
    method: 'get',
    params: paramsList
  }, {
    repeat_request_cancel: false
  })
}

Loading

异步数据是非常常见的场景,一个良好的Loading效果能很好的加强用户体验,也能让我们回避一些问题,如上面提到的重复请求,如果在发起了一个请求后立即就出现一个Loading层,那么用户就无法再次点击而造成重复多次请求了。

添加怎么一个功能我们需要考虑怎么三件事:

  • 同一时间内发起多个请求,我们只需要展示一个Loading层即可,不需要产生多个造成重复展示。
  • 同一时间内发起多个请求展示的Loading层以最后一个请求响应而关闭销毁。
  • 此功能依旧要进行可配置化处理。

废话不多说,我们直接以 ElementPlus 的Loading效果玩耍,具体查看代码相关注释。

// axios.js
const LoadingInstance = {
  _target: null, // 保存Loading实例
  _count: 0
};

function myAxios(axiosConfig, customOptions) {
  const service = axios.create({
    baseURL: 'http://localhost:8888', // 设置统一的请求前缀
    timeout: 10000, // 设置统一的超时时长
  });

  // 自定义配置
  let custom_options = Object.assign({
    repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
    loading: false, // 是否开启loading层效果, 默认为false
  }, customOptions);


  service.interceptors.request.use(
    config => {
      removePending(config);
      custom_options.repeat_request_cancel && addPending(config); 
      // 创建loading实例  
      if (custom_options.loading) {
        LoadingInstance._count++;
        if(LoadingInstance._count === 1) {
          LoadingInstance._target = ElLoading.service();
        }
      }
      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );

  service.interceptors.response.use(
    response => {
      removePending(response.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading
      return response;
    },
    error => {
      error.config && removePending(error.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading
      return Promise.reject(error);
    }
  );

  return service(axiosConfig)
}

/**
 * 关闭Loading层实例
 * @param {*} _options 
 */
function closeLoading(_options) {
  if(_options.loading && LoadingInstance._count > 0) LoadingInstance._count--;
  if(LoadingInstance._count === 0) {
    LoadingInstance._target.close();
    LoadingInstance._target = null;
  }
}

关于上面_count变量的作用还不明白的可以康康,明白的当我没说-.-,假如现在同时发起两个请求,两个请求同时打开了个Loading层,现在有一个请求结束了,关闭了loading层,但是另一个请求由于某些原因并没有结束,还在请求,造成的后果就是页面请求还没完成,loading层却关闭了,用户会以为页面加载完成了,结果页面不能正常运行,导致用户体验不好,所以增加了个变量来记录请求的次数。

当然如果你是杠精那么你又会想如果这个接口是个响应时间比较长,而且获取的数据其实并不影响页面的其他操作,那么一直有个Loading层反而是体验差了。还好我有Plan B,故设计上面的Loading层是个可配置的选项,对于这种情况的API可以选择不用这个页面级别的Loading层,转而自己去具体内使用元素级别的Loading效果更佳。

对于ElementPlus的Loading组件,它还有很多配置参数。

image.png

我们也能自定义处理掉,考虑到简洁单一点我们直接增加第三个参数。

// axios.js
function myAxios(axiosConfig, customOptions, loadingOptions) {
  service.interceptors.request.use(
    config => {
      ...
      // 创建loading实例
      if (custom_options.loading) {
        LoadingInstance._count++;
        if(LoadingInstance._count === 1) {
          LoadingInstance._target = ElLoading.service(loadingOptions);
        }
      }
      return config;
    }, 
    ...
  );
  ...  
}

至此,我们就能给每个需要页面级Loading层的API方法定制不同的Loading层了。(是不是很棒,又很花里胡哨?哈哈哈)

// goods.js
export function getListAPI(paramsList) {
  return myAxios({
    url: '/api/list',
    method: 'get',
    params: paramsList
  }, {
    loading: true
  }, {
    text: '获取列表数据....'
  })
}

判断不同HTTP状态码

一个良好展示接口实时状态的提示信息是非常重要的,开发时方便前端人员定位问题,测试时方便测试人员通知对应人员,在一些复杂特殊场景给予用户提示引导。

在上正式代码前,我们先打印几种接口异常情况,测试代码如下:

// axios.js
function myAxios(axiosConfig, customOptions, loadingOptions) {
  ...
  service.interceptors.response.use(
    ...
    error => {
      ...
      httpErrorStatusHandle(error); // 处理错误状态码
      return Promise.reject(error); // 错误继续返回给到具体页面
    }
  );
  ...
}

/**
 * 处理异常
 * @param {*} error 
 */
function httpErrorStatusHandle(error) {
  console.log('error: ', error);
  console.log('error.message: ', error.message);
  console.log('error.response: ', error.response);
}

需要注意上面打印的 error 本质是个对象来着,但控制台可能不是很明显的表示,底下还有很多属性是我们能用到的,文档也有说哦。

各种异常情况

后端抛错、客户端断网

这种情况一般接口整个就挂了,客户端断网了也会是这种情况,我们能通过 window.navigator.onLine 来判断是否断网了。

// app.js
app.get('/api/list', (req, res) => {
  // console.log(a);
  throw new Error('错误啦!!!');
});

image.png

请求超时

我们更改node服务延时响应来制造超时效果

// app.js
app.get('/api/list', (req, res) => {
  setTimeout(() => {
    res.end();
  },  150000)
});

image.png

3XX 重定向

image.png

image.png

4XX 客户端错误

image.png

image.png

5XX 服务端错误

image.png

image.png

// app.js
app.get('/api/list', (req, res) => {
  // res.statusCode = 302;
  // res.end('重定向');

  // res.statusCode = 400;
  // res.end('请求参数错误');

  res.statusCode = 500;
  res.end('服务器内部错误');
});

上面就大致列了一下常见的各种情况,下面我就直接上代码,也挺简单,只要接口错误,提示对应的错误信息就完了(=^▽^=)。

具体编码

// axios.js
/**
 * 处理异常
 * @param {*} error 
 */
function httpErrorStatusHandle(error) {
  // 处理被取消的请求
  if(axios.isCancel(error)) return console.error('请求的重复请求:' + error.message);
  let message = '';
  if (error && error.response) {
    switch(error.response.status) {
      case 302: message = '接口重定向了!';break;
      case 400: message = '参数不正确!';break;
      case 401: message = '您未登录,或者登录已经超时,请先登录!';break;
      case 403: message = '您没有权限操作!'; break;
      case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
      case 408: message = '请求超时!'; break;
      case 409: message = '系统已存在相同数据!'; break;
      case 500: message = '服务器内部错误!'; break;
      case 501: message = '服务未实现!'; break;
      case 502: message = '网关错误!'; break;
      case 503: message = '服务不可用!'; break;
      case 504: message = '服务暂时无法访问,请稍后再试!'; break;
      case 505: message = 'HTTP版本不受支持!'; break;
      default: message = '异常问题,请联系管理员!'; break
    }
  }
  if (error.message.includes('timeout')) message = '网络请求超时!';
  if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';

  ElMessage({
    type: 'error',
    message
  })
}

编写这个处理异常的方法其实不难,但我们注意要处理一下上面讲过的取消重复请求的情况,取消后的请求也会进入这其中,我们简单的将重复请求的接口打印在控制台即可。

我们借助ElementPlus的Message组件来提示信息,具体提示文案可以自行更改或添加更多情况,也能用接口来定义这些信息,就看具体情况啦。

当然也我们把该功能配置化:

// axios.js
function myAxios(axiosConfig, customOptions, loadingOptions) {
  ...
  // 自定义配置
  let custom_options = Object.assign({
    ...
    error_message_show: true, // 是否开启接口错误信息展示,默认为true
  }, customOptions);
 
  service.interceptors.response.use(
    ...
    error => {
      ...
      custom_options.error_message_show && httpErrorStatusHandle(error); // 处理错误状态码
      return Promise.reject(error); // 错误继续返回给到具体页面
    }
  );
  ...
}

其他实用的小优化

请求自动携带token

这里比较简单就直接上代码了。

// axios.js
import {getTokenAUTH} from '@/utils/auth';
function myAxios(axiosConfig, customOptions, loadingOptions) {
  ...
  service.interceptors.request.use(
    config => {
      ...
      // 自动携带token
      if (getTokenAUTH() && typeof window !== "undefined") {
        config.headers.Authorization = getTokenAUTH();
      }
      return config;
    }, 
    ...
  );
  ...
}
// auth.js
const TOKEN_KEY = '__TOKEN';
export function getTokenAUTH() {
   return localStorage.getItem(TOKEN_KEY);
}

因为我的token是存在本地缓存里面,如果你的token存在store里面,就自行修改修改咯,不要告诉我这你都不会,那你就和那啥没什么区别了。typeof window !== "undefined" 主要是为了兼容ssr的环境情况。

image.png

简洁的数据响应结构

// axios.js
function myAxios(axiosConfig, customOptions, loadingOptions) {
  ...  
  // 自定义配置
  let custom_options = Object.assign({
    repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
    loading: false, // 是否开启loading层效果, 默认为false
    reduct_data_format: true, // 是否开启简洁的数据结构响应, 默认为true
  }, customOptions);
  ...  
  service.interceptors.response.use(
    response => {
      removePending(response.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading

      return custom_options.reduct_data_format ? response.data : response;
    },
    ...
  );
  ...  
}

image.png axios默认返回的响应数据会帮我们包上一层数据,而真正后端返回的数据都在response.data里面,这样有时我们返问数据就要镶嵌很长访问下去,如果中间有一层断了,就容易引起报错了。故我们能设置返回简洁点的数据直接给到具体页面逻辑中,方便使用,通过 reduct_data_format 参数来控制配置。
当然,上面只是简单缩短axios最外层而已,如果你已经很明确且有公司有规定的响应数据结构,那你也能自行再次修改,看具体场景而定。

关于code的错误提示

这点根据要根据具体业务场景而定!!!
很多时候后端接口总有在除HTTP状态码的情况下再定义一个 code 参数决定当前接口是否是“正常”的,一般正常的时候code会等于0,我们先直接上代码再解释。

function myAxios(axiosConfig, customOptions, loadingOptions) {
  ...
  let custom_options = Object.assign({
    ...
    code_message_show: false, // 是否开启code不为0时的信息提示, 默认为false
  }, customOptions);

  service.interceptors.response.use(
    response => {
      ...
      if(custom_options.code_message_show && response.data && response.data.code !== 0) {
        ElMessage({
          type: 'error',
          message: response.data.message
        })
        return Promise.reject(response.data); // code不等于0, 页面具体逻辑就不执行了
      }

      return custom_options.reduct_data_format ? response.data : response;
    },
    ...
  );
  ...
}

简单来说,就是在code不等于0的时候,我们就直接展示后端带来的提示语,当然这要前后端先商量好,固定好响应的数据结构,而具体到页面逻辑里面我们就只处理code等于0的时候那种正常情况。当然我们也通过配置化来设定这个功能,如果前后端定义好数据结构,就直接改了 code_message_show 默认值,就不用一个一个接口去开启,也是很方便的一个功能吧。

完整代码

最后给出 axios.js 完整的代码,肝了两天,写累了,希望对你有所帮助吧。

image.png

import axios from 'axios';
import { ElLoading, ElMessage } from 'element-plus';
import {getTokenAUTH} from '@/utils/auth';

const pendingMap = new Map();

const LoadingInstance = {
  _target: null,
  _count: 0
};

function myAxios(axiosConfig, customOptions, loadingOptions) {
  const service = axios.create({
    baseURL: 'http://localhost:8888', // 设置统一的请求前缀
    timeout: 10000, // 设置统一的超时时长
  });

  // 自定义配置
  let custom_options = Object.assign({
    repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
    loading: false, // 是否开启loading层效果, 默认为false
    reduct_data_format: true, // 是否开启简洁的数据结构响应, 默认为true
    error_message_show: true, // 是否开启接口错误信息展示,默认为true
    code_message_show: false, // 是否开启code不为0时的信息提示, 默认为false
  }, customOptions);

  // 请求拦截
  service.interceptors.request.use(
    config => {
      removePending(config);
      custom_options.repeat_request_cancel && addPending(config); 
      // 创建loading实例
      if (custom_options.loading) {
        LoadingInstance._count++;
        if(LoadingInstance._count === 1) {
          LoadingInstance._target = ElLoading.service(loadingOptions);
        }
      }
      // 自动携带token
      if (getTokenAUTH() && typeof window !== "undefined") {
        config.headers.Authorization = getTokenAUTH();
      }

      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );

  // 响应拦截
  service.interceptors.response.use(
    response => {
      removePending(response.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading

      if(custom_options.code_message_show && response.data && response.data.code !== 0) {
        ElMessage({
          type: 'error',
          message: response.data.message
        })
        return Promise.reject(response.data); // code不等于0, 页面具体逻辑就不执行了
      }

      return custom_options.reduct_data_format ? response.data : response;
    },
    error => {
      error.config && removePending(error.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading
      custom_options.error_message_show && httpErrorStatusHandle(error); // 处理错误状态码
      return Promise.reject(error); // 错误继续返回给到具体页面
    }
  );

  return service(axiosConfig)
}

export default myAxios;

/**
 * 处理异常
 * @param {*} error 
 */
function httpErrorStatusHandle(error) {
  // 处理被取消的请求
  if(axios.isCancel(error)) return console.error('请求的重复请求:' + error.message);
  let message = '';
  if (error && error.response) {
    switch(error.response.status) {
      case 302: message = '接口重定向了!';break;
      case 400: message = '参数不正确!';break;
      case 401: message = '您未登录,或者登录已经超时,请先登录!';break;
      case 403: message = '您没有权限操作!'; break;
      case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
      case 408: message = '请求超时!'; break;
      case 409: message = '系统已存在相同数据!'; break;
      case 500: message = '服务器内部错误!'; break;
      case 501: message = '服务未实现!'; break;
      case 502: message = '网关错误!'; break;
      case 503: message = '服务不可用!'; break;
      case 504: message = '服务暂时无法访问,请稍后再试!'; break;
      case 505: message = 'HTTP版本不受支持!'; break;
      default: message = '异常问题,请联系管理员!'; break
    }
  }
  if (error.message.includes('timeout')) message = '网络请求超时!';
  if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';

  ElMessage({
    type: 'error',
    message
  })
}

/**
 * 关闭Loading层实例
 * @param {*} _options 
 */
function closeLoading(_options) {
  if(_options.loading && LoadingInstance._count > 0) LoadingInstance._count--;
  if(LoadingInstance._count === 0) {
    LoadingInstance._target.close();
    LoadingInstance._target = null;
  }
}

/**
 * 储存每个请求的唯一cancel回调, 以此为标识
 * @param {*} config 
 */
function addPending(config) {
  const pendingKey = getPendingKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingMap.has(pendingKey)) {
      pendingMap.set(pendingKey, cancel);
    }
  });
}

/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config) {
  const pendingKey = getPendingKey(config);
  if (pendingMap.has(pendingKey)) {
     const cancelToken = pendingMap.get(pendingKey);
     // 如你不明白此处为什么需要传递pendingKey可以看文章下方的补丁解释
     cancelToken(pendingKey);
     pendingMap.delete(pendingKey);
  }
}

/**
 * 生成唯一的每个请求的唯一key
 * @param {*} config 
 * @returns 
 */
function getPendingKey(config) {
  let {url, method, params, data} = config;
  if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象
  return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');
}

补丁

在完整源码中,有一个 removePending() 方法,里面有一行 cancelToken(pendingKey); 代码,很多倔友在问为什么需要传递 pendingKey 参数 ?这里做个统一回复哈。

首先,你不传这个参数是完全没有问题的,对代码、程序没有影响的。

其次,它是一个提升开发者好感的选项,咋说?不急听我细细道来。

我们先来看看 Axios 官方的一个例子:

image.png

按小编标的序号,快速看完例子你明白了些什么没有呢?没懂?不急,继续往下看!

cancel() 方法允许传递一个参数,在执行这个方法后,axios 会进入错误状态,我们可以通过手动添加 .catch() 来捕获它,或者通过拦截器的第二个参数来处理它,而且它的 .message 属性能获取 cancel() 传递的值。 (记住这个用法)

提个醒,pendingKey 是由请求路径、请求方法、请求参数组成的,例如:
http://juejin.cn&get&{a:1,b:2}&

而当我们执行了 cancelToken(pendingKey); 后,程序会进入到拦截器下图这个位置:

image.png

当我们允许执行 httpErrorStatusHandle() 方法的时候,方法里面会有一段处理代码:

function httpErrorStatusHandle(error) {
  // 处理被取消的请求
  if(axios.isCancel(error)) return console.error('请求的重复请求:' + error.message);
  ...
}

这段代码的作用就是:在控制台中打印出被取消了的请求的信息,这能更好的帮助开发者定位那些请求是会造成重复请求的。

最后,放个效果图,当然你不传的话,提示信息这里只是会变成 undefined 而已,不会有什么影响。

image.png

稍微讲解得有点啰嗦,希望你能明白哈,还是不懂的话,欢迎评论区给我留言。(^▽^)



至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。