如何高效管理axios的api接口

630 阅读7分钟

起个头吧

嘻嘻,想到好东西就是想跟大家一起分享(本文适合会使用vue和axios的码友),顺便装(找)逼(骂),没错,就是皮痒了;

就喜欢你看不惯我又干不掉我的样子

本文主要想分享以下几个点:

  • axios的二次封装
  • axios处理token过期并自动刷新token,不用跳转到登录页重新登录
  • axios中断页面跳转后当前正在进行还没响应的请求
  • api的自动化管理

axios的二次封装

我看很多人都在问,axios的封装都已经够好了,为什么还要进行二次封装?

举个现实生活中的栗子:

在付现金的时代,去饭店吃个小面后结账,服务员都是上来给你算多少钱(8块),然后你给服务员10块,服务员跑去前台给你找2元补给你;

然后老板觉得这样太浪费时间了,就让服务员身上装一些零钱,下次你再去的时候,就直接补给你了;

你考虑得越周到,你的工作效率就会越高;axios考虑的是满足所有用户,而你应该考虑的是满足你自己,怎样才能更高、更快、更狠的完成项目。

回归正题,先直接看代码吧~

let promiseArr = {};
const CancelToken = axios.CancelToken;
//状态
const statusMap = {
  400: '请求参数有误',
  401: '未授权,请重新登录',
  403: '访问被拒绝',
  404: '地址不存在,找不到对应资源',
  405: '请求方法不允许',
  500: '服务端错误',
  501: '网络未实现',
  502: '网络错误',
  503: '服务不可用',
  504: '网络超时',
  505: 'http版本不支持该请求'
}

function request(options) {
  let _url = options.url;
  //防止重复请求
  if(_url in promiseMap){
    promiseMap[_url]();
  }
  let defaultOptions = {
    tip:errorAutoTip,
    cancelToken: new CancelToken(c => {
      promiseMap[_url] = c;
    }),
    url:getGateWayName(options.gateway)+_url,
  };
  delete options.gateway;
  delete options.url;
  defaultOptions = Object.assign(defaultOptions,options);
  let tip = defaultOptions.tip;
  return new Promise((resolve, reject) => {
    axios(defaultOptions).then(res => {
      delete promiseMap[_url];
      //请求成功,需要通过state来判断,不同的后台使用插件可能不同,注意此处需要更正,否则全是reject
      if (res.data.state) {
        resolve(res.data.data);
      }
      //请求失败,由于后台封装的问题,他们抛出的错误与http的状态不符,会出现在then里面 
      else {
        reject(res.data);
        handleCatchError(res, tip);
      }
    }).catch(err => {
      delete promiseMap[_url];
      reject(err.response);
      handleCatchError(err.response, tip);
    })
  })
}

//捕获错误处理
function handleCatchError(obj, tip) {
  if (!isObject(obj)) return;
  let data = obj.data || {};
  //根据后台返回数据提示
  let code = data.code || obj.status;
  let errorInfo = ['message:' + (data.message)];
  code && errorInfo.push(['code:', code, '(', statusMap[code], ')'].join(''));
  tip ? (showMessage.show({ messages: errorInfo })) : (console.log(errorInfo.join('\n')));
}

//网关服务
function getGateWayName(name) {
  const gatewayMap = {
    auth: '/phjr-manager-service'
  }
  return baseUrl + ((name in gatewayMap) ? gatewayMap[name] : '');
}

export const FINAL_SERVICE = {
  // 序列化的方式,参数跟地址栏一起
  get: async function (options) {
    options.url = options.url + stringifyObj(options.data);
    options.data = {};
    return await request(options);
  },
  // 参数实体的方式
  post: async function (options) {
    // 如果不是表单格式的话,就不JSON序列化
    if (options.headers!==null) {
      options.data = JSON.stringify(options.data);
    }
    return await request(options);
  }
}

get、post两种方法,实际意义上应该是两种方式,一种是参数在地址栏上,一种是参数为独立实体,这个根据后端的接口定义有关,所以get、post对于get/post/put/delete都适用,一般情况来说get/delete为一组,post/put为一组;

封装axios为request的目的有4个:

  1. 在开发环境请求接口错误自动提示(可配置),方便调试(showMessage为自定义调用组件的方法,可以使用其他ui插件替换,如iview的$Message,vant的Toast等)
  2. 处理网关服务,后台可能存在多个网关服务,需要配置并添加到url地址(这个功能看自己公司的需求,一般小项目是不需要的,到时候不配置就可以)
  3. 为每个接口配置中断请求项-->cancelToken(请求完成后删除),然后在请求拦截以及路由切换时取消没有响应的请求(下面有讲)
  4. 根据后台返回的状态来判断是否为成功(直接通过axios的.then.catch是有问题的)

axios无痛刷新

大概理一下思路,就是用户正常访问时,token正好过期了,后台返回一个状态(401),然后就去请求后台提供的一个可以刷新token的接口(refreshToken),请求成功之后后台返回最新的token信息,重新存入到storage或者cookie中(此时用户还没有得到反馈,这一步是在axios拦截中做的),通过axios重新请求用户访问的接口最后响应给用户;

废话不多说,直接上代码~

// axios 响应拦截器
axios.interceptors.response.use(
  response => {
    if (response.data && response.data.code === 401) {
      // handleError();
      return handle401(response);
    }
    // 对响应数据做点什么
    return Promise.resolve(response);
  },
  error => {
    if (error.response && error.response.status === 401) {
      // handleError();
      return handle401(error);
    }
    // 对响应错误做点什么
    return Promise.reject(error);
  }
)

function handle401(error) {
  // 如果是刷新token的接口报错401就直接退出
  if (error.config.url.includes('refreshToken')) {
    handleError();
    return Promise.reject(error);
  }
  //如果已经在登录页面了,就不用继续往下了
  if (router.currentRoute.name !== 'login') {
    //那么去请求新 token并返回重新请求的接口
    return doRequest(error);
  }
}

//accessToken过期,通过refreshToken 重置accessToken和refreshToken
async function handleRefreshToken() {
  return await axios.get(getGateWayName('auth') + '/refreshToken');
}
//捕获异步后返回数据{err,data}
async function asyncCapture(asyncFn,params){
    let err='',data={};
    try{
      data = await asyncFn(params);
    }catch(e){
      err = e
    }
    return {err,data};
}
// 处理刷新token后重新获取接口数据,并返回到页面
async function doRequest(error) {
  const { err, data } = await asyncCapture(handleRefreshToken);//请求刷新token的接口
  if (err) {
    handleError();
    return false;
  }
  let { accessToken, refreshToken, expire } = data;
  setToken(accessToken, expire);
  setRefToken(refreshToken, expire);
  let config = error.response.config;
  config.headers.token = accessToken;
  //将请求失败的config重新请求一次
  return await axios.request(config);
}

//过滤失败(token未通过验证)直接退出
function handleError() {
  showMessage.show('登录信息已失效,请重新登录');
  store.dispatch('logout', false);
}

注释都写的很明显,就不废话了,要注意以下几点:

  1. 重新请求refreshToken有可能会失败,要捕获到这个异常机制,否则就容易陷入死循环,要根据实际项目的接口做判断
  2. 正常响应response和异常响应error都要做判断,因为后台返回的状态和http状态会不一致

中断axios请求

上文提到中断请求应该有2种情况,一是用户请求同一个接口,由于响应速度慢,上一个接口还没响应,同一个接口又发起了请求;二是同样是接口响应慢导致,用户在当前页面请求的接口都没响应,结果就跳转了页面,当前页面的接口应该全部中断。

实现方式就可以针对这2种情况来开展:

  1. 通过路由拦截,遍历当前的所有请求并中断
//router 路由拦截
router.beforeEach((from,to,next)=>{
  let item;
  for(item in promiseMap){
    promiseMap[item]();
  }
  promiseMap = {};
  next();
})
  1. 请求中判断上次请求是否存在,存在则中断
function request(options) {
  let _url = options.url;
  //防止重复请求
  if(_url in promiseMap){
    promiseMap[_url]();
  }
  ......
}

api的自动化管理

接着上个故事继续讲

随着科技的进步,以前饭店还需要收银员去收钱,然后补钱,但是两个马爸爸就觉得,不行呀,就算你准备了零钱也要多个步骤啊,我还要腾出手来收钱补钱,那我给你一个二维码吧,你自己付,我就只管给你们做吃的就可以了。

自动化管理就是如此,我看网上的方式其实都差不多,都是封装axios,然后根据需求定义api文件,再挂载到vue实例或者window全局上,方式都差不多,不过我还是喜欢按照自己的思路去封装,怎么简单怎么来,怎么好维护怎么来,怎么能偷懒怎么来;

1. index.js

/**
 * 使用require.context自动导入api结尾的文件,并全部注入到APIS一个对象中,方便全局导入
 */
const ctx = require.context('.',false,/.api.js$/);
const APIS = {};
ctx.keys().forEach((item)=>{
  let name = item.split('.')[1].slice(1);
  let config = ctx(item);
  let dft = config.default || config;
  APIS[name] = dft;
})
export default APIS;
/*
user.api.js  order.api.js
{
    user:{...很多方法},
    order:{...很多方法}
}
*/

自动读取api文件夹下的.api.js后缀的文件,并自动归类名称,例如我新建一个cart.api.js,然后就会新生成一个属性cart,并且不会出现命名冲突的问题,如果需要注册到vue或window,直接引入即可:

//main.js
import api from './api/index.js';//引入所有的对象
/*
{
    user:{
        get:f()
        ...
    },
    order:{...},
    cart:{
        get:f(),
        ...
    }
}
*/
vue.prototype.$api = api;//vue
// or window.$api = api;//window

2. urlHandler.js

// 引入全局请求方法
import { FINAL_SERVICE } from './request';

//异步处理函数
async asyncCapture(asyncFn,params){
    let err='',data={};
    try{
      data = await asyncFn(params);
    }catch(e){
      err = e
    }
    return {err,data};
}
/**
 * 获取数据类型
 */
const getType = (function () {
  return function (val) {
    let str = Object.prototype.toString.call(val);
    return str.slice(8, str.length - 1).toLowerCase();
  }
})();

export function urlHandler(urls){
    const finalExport = {};
    urls.forEach(item => {
        let name = item.name
        if (!name || (name in finalExport)) {
            throw new Error(name+' should be required and unique,but be multiple or undefined!')
        }
        finalExport[item.name] = function (params) {
            //如果url是个方法的话,单独处理
            if(getType(item.url)==='function'){
                item.url = item.url(params);
            }else{
                item.data = params;
            }
            //返回异步数据{err,data}
            return asyncCapture(FINAL_SERVICE[item.way],item);
        }
    })
    // console.log(finalExport)
    return finalExport;
}

3. user.api.js

// 引入全局请求方法
import {urlHandler} from './urlHandler';

const urls = [
  { 
    name: 'login', //函数名称
    url: '/auth/login', //请求地址
    method:'post', //请求方法
    gateway: 'auth', //请求网关服务
    way: 'post', //请求处理方式,post为实体方式,get为序列化方式,
    tip:false,//请求失败是否自动提示,默认是可配置(config.js)
  },
  { 
    name: 'getMe', 
    url: '/out/me/get', 
    method: 'get',
    gateway: 'auth',
    way: 'get',
  },
  { 
    name: 'updateMe', 
    url: '/out/me/update', 
    method: 'put', 
    gateway: 'auth',
    way: 'post' 
  },
  { 
    name: 'deleteMe', 
    url:({id})=>{
      return '/out/me/delete/'+id
    }, 
    method: 'delete', 
    gateway: 'auth',
    way: 'get' 
  },
]

export default urlHandler(urls);
/*
{
    login:f(),
    getMe:f(),
    updateMe:f(),
    deleteMe:f()
}
*/

这种方式,好处真的很多

  1. 可以添加自定义字段进行单独处理(只要不和axios的字段冲突)

  2. 配置与axios冲突的字段,对axios的进行覆盖,例如method,url,headers,timeout,data等等

  3. 可以自定义字段的类型,String,Function等,只需要在公用方法上进行处理即可,可维护性更高

    上面对url进行了自动处理,可以给url配置string和function,在接口访问时,除了序列化方式(interface?a=1&b=2),还有直接在后面加参数的情况(/interface/:id/:name),这种就需要进行单独处理

  4. 重复代码很少,就相当于定义一个数组对象

使用试试?

<template>
  <div class="width-limit">
    <h1>请求测试</h1>
    <me-button class="margin-r-base" @click="request('get')">get请求</me-button>
    <me-button class="margin-r-base" @click="request('post')">post请求</me-button>
    <me-button class="margin-r-base" @click="request('put')">put请求</me-button>
    <me-button @click="request('delete')">delete请求</me-button>
  </div>
</template>

<script>
import userApi from '../api/user.api';//按需引入,前提是没有全局引入(为了测试,同时也注册到vue上)
export default {
  methods: {
    request(method) {
      switch (method) {
        case "get": {
          const {err,data} = await userApi.getOrder({ page:1,rows:10 });
          if(!err){
            console.log(data)
          }
          break;
        }
        case "post": {
          userApi.login({account:'test',password: "123456",kaptcha:'de56' })
            .then(({err,data})=>{
              if(err) throw err;
              console.log(data);
            });
          break;
        }
        case "put": {
          this.$api.user.updateMe({ id:1,count:2 });
          break;
        }
        case "delete": {
          this.$api.user.deleteMe({ id:1 });
        }
      }
    }
  }
};
</script>

处理请求结果有2种处理方式:

  1. 配合async await:
aysnc ...
    const {err,data} = await userApi.getMe({ page:1,rows:10 });
    if(!err){
        console.log(data)
    }
  1. promise.then链式模式:
userApi.login({account:'test',password: "123456",kaptcha:'de56' })
.then(({err,data})=>{
  //err和data封装在同一个对象返回,这样在处理相同情况时就省去一个步骤
  /*
    例如:点击触发请求后使loading=true,当返回结果时,无论是成功还是失败,都应该将loading=false
  */
  if(err) throw err;
  console.log(data);
});

看看效果

页面:

get请求:

post请求:

这里可以发现,请求响应明明是返回的成功200,而后台却返回的错误500,所以当我对axios进行二次封装后,我已经捕获到这是一个错误的请求,并抛出;

delete请求:

delete请求接口格式也按照自己封装的形式进行请求,(由于测试,就不进行真实删除)404报错,自动提示错误信息;

结个尾吧

本文所叙述的都是实际工作中遇到的问题,然后通过各种途径想办法进行解决,不管是请教同事、看博客、看书还是看视频,最后还是的总结成自己的一套思路,满足公司的实际需求。长路漫漫,其修远兮!技术的沉淀是来源于实际运用过程中对遇到问题的思考和总结。

最后给大家奉上我的源码吧~github地址,如果有什么不好的地方,请大家多多指正,大家共同进步。