axios封装

问题背景

在新项目开始时,axios的封装是必须的,这里就总结回顾一下axios都需要进行哪些封装把

基础配置(参考vue-element-admin,可直接使用)

1.请求自动携带token
2.统一处理错误情况
3.默认去除response的包装,只返回data。通过meta的responseAll配置为true获取所有的response

其中请求拦截器的逻辑为:如果用户登陆了有token,则在请求头上携带token
其中响应拦截器的逻辑为:
返回的code是否为200
    是:根据配置返回全部的res或者直接返回data
    否:message提示用户,抛出异常
           并且同时判断是否为token有问题的情况
                有问题:让用户确认是否退出
                    确认退出:调用vuex退出系统的方法并重新加载一下login页面

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  config => {
    // 如果登录了,有token,则请求携带token
    // Do something before request is sent
    if (store.state.userInfo.token) {
      config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// respone拦截器
service.interceptors.response.use(
  // response => response,
  /**
   * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
   * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
   */
  response => {
    const res = response.data
    // 处理异常的情况
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000,
      })
      // 403:非法的token; 50012:其他客户端登录了;  401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning',
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      // 默认只返回data,不返回状态码和message
      // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
      const isbackAll = response.config.meta && response.config.meta.responseAll
      if(isbackAll){
        return res
      }else{
        return res.data
      }
    }
  },
  error => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

export default service

如果不想配置过多的axios,那么上面的代码已经可以满足需求了。搭配使用方式为:
axios封装_第1张图片

优化一:取消重复请求

参考地址:https://juejin.cn/post/6968630178163458084#heading-7
发生重复请求的场景一般有这两个(主要还是tab的切换会导致数据错乱):

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

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

效果:同时发送三个请求,出现这个canceled就是成功的把前两次请求给取消了

axios封装_第2张图片

需要用到的几个函数

// 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);
    }
  });
}
/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config) {
  const pendingKey = getPendingKey(config);
  if (pendingMap.has(pendingKey)) {
     const cancelToken = pendingMap.get(pendingKey);
     cancelToken(pendingKey);
     pendingMap.delete(pendingKey);
  }
}

axios简化版代码(方便看上面方法加入的位置):

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

  service.interceptors.request.use(
    config => {
      // 删除重复的请求
      removePending(config);
      // 如果repeatRequest不配置,那么该请求则不能多次请求
      !config.repeatRequest && 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);
    }
  );

加入到基础配置后的代码:

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  config => {
    removePending(config)
    // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
    !config.repeatRequest && addPending(config)
    // 如果登录了,有token,则请求携带token
    // Do something before request is sent
    if (store.state.userInfo.token) {
      config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// respone拦截器
service.interceptors.response.use(
  // response => response,
  /**
   * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
   * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
   */
  response => {
    // 已完成请求的删除请求中数组
    removePending(response.config);

    const res = response.data
    // 处理异常的情况
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000,
      })
      // 403:非法的token; 50012:其他客户端登录了;  401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning',
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      // 默认只返回data,不返回状态码和message
      // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
      const isbackAll = response.config.meta && response.config.meta.responseAll
      if(isbackAll){
        return res
      }else{
        return res.data
      }
    }
  },
  error => {
    error.config && removePending(error.config)
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

// 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);
    }
  });
}
/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config) {
  const pendingKey = getPendingKey(config);
  if (pendingMap.has(pendingKey)) {
     const cancelToken = pendingMap.get(pendingKey);
     cancelToken(pendingKey);
     pendingMap.delete(pendingKey);
  }
}

export default service

搭配使用api使用,比如某个请求是可以同时进行请求的:
axios封装_第3张图片
提问:取消了接口的重复请求,还有必要做按钮的防抖节流嘛?
回:不一定,取消重复请求只是在前端过滤了这个请求,后端还是收到了2份请求的,如果是表单类的提交,那数据库还是会生成2条数据的

优化二:配置loading

参考:https://blog.csdn.net/weixin_43239880/article/details/121688263?spm=1001.2014.3001.5501
为什么需要在axios中配置loading?
原因:每次发请求都需要去配置一个值,然后请求开始将该值设为true,请求完毕设为false。感觉很麻烦
效果:发送请求时加入配置:可以让任意一个盒子loading

const res3 = await getListById({ loading: true, loadingDom: ".bg3" })

axios封装_第4张图片
需要用到的函数

const LoadingInstance = {
  _target: null, // 保存Loading实例
  _count: 0, // 计算数量,保证一次只有一个loading
}
function openLoading(loadingDom) {
  LoadingInstance._target = Loading.service({
    lock: true,
    text: '数据正在加载中',
    spinner: 'el-icon-loading',
    background: 'rgba(25, 32, 53, 1)',
    target: loadingDom || 'body',
  })
}
function closeLoading() {
  if (LoadingInstance._count > 0) LoadingInstance._count--
  if (LoadingInstance._count === 0) {
    LoadingInstance._target.close()
    LoadingInstance._target = null
  }
}

axios简化版代码(方便看上面方法加入的位置):

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

  service.interceptors.request.use(
    config => {
       // 打开loading
    if (config.loading) {
      LoadingInstance._count++
      if(LoadingInstance._count === 1){
        openLoading(config.loadingDom)
      }
    }
      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );

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

加入到基础配置后的代码。这也是最终的完整代码:

import axios from 'axios'
import { Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  config => {
    removePending(config)
    // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
    !config.repeatRequest && addPending(config)
    // 打开loading
    if (config.loading) {
      LoadingInstance._count++
      if(LoadingInstance._count === 1){
        openLoading(config.loadingDom)
      }
    }
    // 如果登录了,有token,则请求携带token
    // Do something before request is sent
    if (store.state.userInfo.token) {
      config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// respone拦截器
service.interceptors.response.use(
  // response => response,
  /**
   * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
   * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
   */
  response => {
    // 已完成请求的删除请求中数组
    removePending(response.config)
    // 关闭loading
    if (response.config.loading) {
      closeLoading()
    }

    const res = response.data
    // 处理异常的情况
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000,
      })
      // 403:非法的token; 50012:其他客户端登录了;  401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning',
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      // 默认只返回data,不返回状态码和message
      // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
      const isbackAll = response.config.meta && response.config.meta.responseAll
      if (isbackAll) {
        return res
      } else {
        return res.data
      }
    }
  },
  error => {
    error.config && removePending(error.config)
    // 关闭loading
    if (error.config.loading) {
      closeLoading()
    }
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

// --------------------------------取消接口重复请求的函数-----------------------------------
// 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)
      }
    })
}
/**
 * 删除重复的请求
 * @param {*} config
 */
function removePending(config) {
  const pendingKey = getPendingKey(config)
  if (pendingMap.has(pendingKey)) {
    const cancelToken = pendingMap.get(pendingKey)
    cancelToken(pendingKey)
    pendingMap.delete(pendingKey)
  }
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance = {
  _target: null, // 保存Loading实例
  _count: 0,
}
function openLoading(loadingDom) {
  LoadingInstance._target = Loading.service({
    lock: true,
    text: '数据正在加载中',
    spinner: 'el-icon-loading',
    background: 'rgba(25, 32, 53, 1)',
    target: loadingDom || 'body',
  })
}
function closeLoading() {
  if (LoadingInstance._count > 0) LoadingInstance._count--
  if (LoadingInstance._count === 0) {
    LoadingInstance._target.close()
    LoadingInstance._target = null
  }
}

export default service

loading搭配api使用:
axios封装_第5张图片
loading页面中使用:
axios封装_第6张图片

优化三:用qs模块来序列化参数

我这边没有使用qs进行序列化。主要原因是因为现在前后端交互的数据格式主流就是json格式。只有图片上传接口会使用表单的方式进行提交。 如果是个别的post接口,后台要求你用表单提交,你可以进行交涉下。
前后端交互格式参考https://blog.csdn.net/qq_43654065/article/details/114642300

最终的完整代码

import axios from 'axios'
import { Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  config => {
    removePending(config)
    // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
    !config.repeatRequest && addPending(config)
    // 打开loading
    if (config.loading) {
      LoadingInstance._count++
      if(LoadingInstance._count === 1){
        openLoading(config.loadingDom)
      }
    }
    // 如果登录了,有token,则请求携带token
    // Do something before request is sent
    if (store.state.userInfo.token) {
      config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// respone拦截器
service.interceptors.response.use(
  // response => response,
  /**
   * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
   * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
   */
  response => {
    // 已完成请求的删除请求中数组
    removePending(response.config)
    // 关闭loading
    if (response.config.loading) {
      closeLoading()
    }

    const res = response.data
    // 处理异常的情况
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000,
      })
      // 403:非法的token; 50012:其他客户端登录了;  401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning',
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      // 默认只返回data,不返回状态码和message
      // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
      const isbackAll = response.config.meta && response.config.meta.responseAll
      if (isbackAll) {
        return res
      } else {
        return res.data
      }
    }
  },
  error => {
    error.config && removePending(error.config)
    // 关闭loading
    if (error.config.loading) {
      closeLoading()
    }
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

// --------------------------------取消接口重复请求的函数-----------------------------------
// 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)
      }
    })
}
/**
 * 删除重复的请求
 * @param {*} config
 */
function removePending(config) {
  const pendingKey = getPendingKey(config)
  if (pendingMap.has(pendingKey)) {
    const cancelToken = pendingMap.get(pendingKey)
    cancelToken(pendingKey)
    pendingMap.delete(pendingKey)
  }
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance = {
  _target: null, // 保存Loading实例
  _count: 0,
}
function openLoading(loadingDom) {
  LoadingInstance._target = Loading.service({
    lock: true,
    text: '数据正在加载中',
    spinner: 'el-icon-loading',
    background: 'rgba(25, 32, 53, 1)',
    target: loadingDom || 'body',
  })
}
function closeLoading() {
  if (LoadingInstance._count > 0) LoadingInstance._count--
  if (LoadingInstance._count === 0) {
    LoadingInstance._target.close()
    LoadingInstance._target = null
  }
}

export default service

总结

基础的axios即可满足大部分项目的需求,配置loading和取消重复请求就见仁见智了,可加可不加。

项目demo源码地址:https://github.com/rui-rui-an/packageAxios

你可能感兴趣的:(vue.js,前端,javascript)