如何更加完善的封装axios请求:防抖拦截、无感刷新token、各环境baseUrl配置等

前言

先看下常见的封装请求及使用方法:

1.新建request.js,内容为:

import axios from 'axios'

const service = axios.create({
  baseURL: '',
  timeout: 3000,
  headers: {}
})
service.interceptors.request.use(
  config => {
  // 这里是添加token、判断登录权限等等一些处理逻辑
  return config
},
  error => {
    Promise.reject(error)
  }
)

service.interceptors.response.use(
  res => {
  // 这里是一些处理逻辑
  return res.data
},
  error => {
    return Promise.reject(error)
  }
)
export default service

2.新建一些api文件如 /api/user.js,内容为:

import request from '@/utils/request'

// 用户登录
export function userLogin(data) {
  return request({
    url: '/api/user/login',
    method: 'POST', // post请求
    data
  })
}

// 获取列表信息
export function userList(id) {
  return request({
    url: '/api/user/list?id='+id,
    method: 'GET' // get请求
  })
}

// 文件下载类
export function fileMyDownload(data) {
  return request({
    url: '/api/file/my/file/download/' + data,
    method: 'GET',
    responseType: 'blob'
  })
}

3.页面引入使用

import {userLogin,userList,fileMyDownload} from '/api/user.js'

//页面中的请求方法
userLogin(data).then()
userList(data).then()
fileMyDownload(data).then()

小结

上面这写法api下全是export function,接口多了然后就是几百成千行类似的代码或者是被拆分成几十个js文件。页面使用中也需根据实际import或移除相应的接口方法名称。对于这问题可继续优化完善。

优化

优化api

首先将整合所有的export function方法,将多个export function封装为getRequest。优化为:

import request from '@/utils/request'

const api={
  userLogin:'/user/login',
  userList:'/user/list',
  getUserList:'get|/user/list', // 不同请求类型可在url前添加,使用|分隔
  getUserListById:'/user/list/$id' // 动态参数id时,可添加$标识符
  download:'/user/my/file/download'
}

export const getRequest = (apiKey,data, options= {}) => {
  let url = api[apiKey] || apiKey
  if (Object.keys(options.apiKey || {}).length) {
    for (const key in options.apiKey) {
      url = url.replace(key, options.apiKey[key])
    }
  }
  let method = 'POST' // 默认请求方式
  // 如果url带有|分隔符,提取|前面的作为请求method并过滤掉|前面的
  if (url.indexOf('|') !== -1) {
    method = url.split('|')[0]
    url = url.replace(/.*\|/, '')
  }
  return Object.assign(
    {
      url: '/api'+ url, // 添加前缀
      method: method,
      data
    },
    options
  )
}

页面使用

import { getRequest } from '/api/getRequest'

  export default {
    mounted() {
      const data = {}
      // 1.正常请求,以post方式请求/api/user/login
      getRequest('userLogin', data).then()
      // 2.以get方式请求/api/user/list。同时可在request.js中设置将data转为params,即追加到url后面
      // 2.1 url接口前添加get|
      getRequest('getUserList', data).then()
      // 2.2使用options参数
      getRequest('userList', data, { method: 'get' }).then()
      // 2.3不使用api定义的,直接使用接口url
      getRequest('get|/user/list', data).then()
      // 或
      getRequest('/user/list', data, { method: 'get' }).then()
      //3.url带参数的,使用options参数的apikey,即可正常请求/api/user/list/1
      getRequest('getUserListById', data, { apiKey: { $id: '1' } }).then()
      //4.数据流下载类,添加responseType,同时还可以定义不同的headers
      getRequest('download', data, { responseType: 'blob', headers: {} }).then()
    }
  }

经过上面优化封装,可解决所有不同的请求,可大量减少类似的重复代码,更方便阅读。同步的request.js可优化为:

除了常见的添加token请求头外,还增加了:

1. 在get请求时将data参数转为params方式,直接拼接到url后面;

3. 请求防抖拦截,同一个接口和参数在一定时间内不重复发请求

4. 无感刷新token

5.不同环境baseUrl配置

import * as md5 from 'md5'

//5 通过运行的命令获取各不同环境的请求api等,无需在根目录添加如.env.development等文件
const mode = import.meta.env.MODE
const baseUrl: any = {
  development: '', // 默认命令dev
  production: '', // build
  preRelease: '' // 自定义其他环境如 "build:pre": "vite build --mode preRelease"
}

const service = axios.create({
  baseURL: baseUrl[mode],
  timeout: 3000, // request timeout
  headers: {}
})

const axiosList = {}

/***************************无感刷新换token相关****************/
let refreshTokenAjax: boolean = false
// 存储请求的数组
const subscribesArr = []

// 请求push到数组中
function subscribesArrRefresh(cb) {
  subscribesArr.push(cb)
}

// 用新token发起请求
function reloadSubscribesArr(newToken) {
  subscribesArr.map(cb => cb(newToken))
}

// 使用refreshToken请求获取新的token
function getNewToken(refreshToken) {
 //根据接口需求,处理刷新所需参数
  const params: any = { refreshToken: refreshToken }
  axios
    .post('/mock/system/user/token.json', params)
    .then((result: any) => {
      // 刷新成功,这里需要将新的token保存
      // .... 一些保存逻辑
      reloadSubscribesArr(data.token)
      refreshTokenAjax = false
    })
    .catch(() => {
      //... 换取token失败的处理逻辑,一般为退出到登录页
      refreshTokenAjax = false
    })
}
/***************************无感刷新换token相关结束****************/

service.interceptors.request.use(
  config => {
    //1. get请求时,将参数放到url后面
    if (config.method.toUpperCase() === 'GET') {
      config.params = config.data
    }
    //2. 让每个请求携带自定义token 请根据实际情况自行修改
    const token = '' // 一般从store或storage里获取
    if (token) {
      config.headers['Authorization'] = token
    }
    //3. 全局防抖拦截,请根据实际情况自行修改
    const urlKey = md5(config.url + JSON.stringify(config.data || {}))
    const lastTime = axiosList[urlKey]
    if (lastTime) {
      const diffTime = new Date().getTime() - lastTime
      if (diffTime < 1500) {
        //间隔时间,小于此时间视为重复提交
        return Promise.reject({ code: 1, msg: '数据正在处理,请勿重复提交' })
      }
    }
    axiosList[urlKey] = new Date().getTime() // 保持或更新请求记录
    // 添加记录,然后自动清除
    setTimeout(() => {
      delete axiosList[urlKey]
    }, 5000)
    // 全局防抖拦截结束
   // 4.无感刷新token开始
  const refreshToken = '' // 一般从store或storage里获取刷新token所需的refreshToken 
    if (!token && refreshToken) { //token过期逻辑判断
      if (!refreshTokenAjax) {
        getNewToken(refreshToken)
      }
      refreshTokenAjax = true
      return new Promise(resolve => {
        subscribesArrRefresh(newToken => {
          config.headers['Authorization'] = newToken
          resolve(config)
        })
      })
    }
  // 无感刷新token结束
    return config
  },
  error => {
    Promise.reject(error)
  }
)

总结

至此封装一个getRequest方法即满足所有接口请求,新增接口url时直接修改api={}即可,在页面引入使用时也无需单独引入,统一引入同一方式即可。减少了大量的代码

GitHub - 337547038/vue-form-design: ak-design可视化低代码快速开发平台,使用基于vue3.x桌面端组件库 Elemnet-Plus,通过可视化的操作,可轻松快速完成拖拽表单编辑设计器、数据列表页设计、流程管理设计器、数据大屏可视化拖拽设计编辑器、数据统计拖拽设计;提供功能强大的各类组件,可适用于各种复杂场景,代码简洁、易于二次开发;用于学习研究,欢迎交流,微信:337547038