用在后台管理系统的一个公共请求组件,解决重复换取token,过滤重复请求,切换组件取消请求队列

后台管理系统的技术栈:vite、vue3、pinia、vue-router、naive-ui。
用在系统中的请求组件,用到了 axios,lodash,pinia 这几个库,解决了几个开发中的痛点:
1.token 过期需要更新 token,当token正在换取时,并发请求过多导致重复换取新 token 问题
2.重复请求过滤
3.切换组件时取消还未完成的请求

直接上代码

import axios from 'axios';
import _ from 'lodash';
// 使用pinia管理状态
import pinia from './pinia';
// 公共loading
import { useLoadingStore } from '../store/loading';
// 用户token
import { useUserStore } from '../store/user';
// 请求队列
import { useRequestStore } from '../store/request';

const userStore = useUserStore(pinia),
  requestStore = useRequestStore(pinia),
  loadingStore = useLoadingStore(pinia);

// 接口地址
const apiHost = 'https://xxx.com';
// 请求根目录,使用了环境变量配置文件,打生产包时会使用.env.production文件中的VITE_API_BASEURL地址
let baseURL = import.meta.env.MODE === 'production' ? import.meta.env.VITE_API_BASEURL : apiHost;

// 初始化axios实例
let http = axios.create({
  baseURL,
  timeout: 60000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8;'
  }
});

// 并发请求数量,处理多个并发请求时,多次调用loading问题
let requestCount = 0;
// loading操作
const ShowLoading = () => {
  if (requestCount === 0) {
    $uiMsg.loading('加载中');
  }
  requestCount++;
};
const HideLoading = () => {
  requestCount--;
  if (requestCount <= 0) {
    requestCount = 0;
    $uiMsg.removeMessage();
  }
};

// 根据url和参数生成标识key
const GenerateKey = config => {
  let { method, url, params, data } = config;
  return [method, url, params, typeof data === 'object' ? JSON.stringify(data) : data].join('&');
};
// 阻止重复请求
const StopRepeatRequest = (config, cancel, errorMessage) => {
  const errorMsg = errorMessage || '';
  for (let i = 0; i < requestStore.list.length; i++) {
    if (requestStore.list[i].key === GenerateKey(config)) {
      cancel(errorMsg);
      return;
    }
  }
  requestStore.Add({
    key: GenerateKey(config),
    cancel
  });
};
// 请求完成后删除重复队列
const DelRequest = config => {
  for (let i = 0; i < requestStore.list.length; i++) {
    if (requestStore.list[i].key === GenerateKey(config)) {
      requestStore.Del(i);
      break;
    }
  }
};

http.interceptors.request.use(config => {
  let cancel;
  config.cancelToken = new axios.CancelToken(c => {
    cancel = c;
  });
  StopRepeatRequest(config, cancel, `${config.url} 请求被中断`);
  if (userStore.token) {
    config.headers.Authorization = 'Bearer ' + userStore.token;
  }
  return config;
});

// 处理token过期时重复换取token问题
let isRefreshToken = false,
  retryRequest = [];
http.interceptors.response.use(
  response => {
    loadingStore.Hide();
    DelRequest(response.config);
    if (response.config.url.indexOf('/token') !== -1 && response.data.code !== 200) {
      // 换token时报错,退出登录
      $uiMsg.error('登录已过期,请重新登录', () => {
        userStore.LoginOut();
      });
    }
    return response.data;
  },
  error => {
    loadingStore.Hide();
    HideLoading();
    if (error.response && error.response.status === 401) {
      // token已过期,开始换取新token
      const config = error.response.config;
      // 删除队列
      DelRequest(config);
      if (!isRefreshToken) {
        // 处于未换取token状态
        isRefreshToken = true;
        return http
          .post('/token', userStore.token)
          .then(res => {
            if (!res.body) return;
            userStore.LoginIn(res.body);
            config.headers['Authorization'] = 'Bearer ' + res.body.token;
            // 已经刷新token,执行等候队列中的请求
            retryRequest.forEach(async cb => await cb('Bearer ' + res.body.token));
            // 执行完清空队列
            retryRequest = [];
            isRefreshToken = false;
            return http(config);
          })
          .catch(() => {
            isRefreshToken = false;
          })
          .finally(() => {
            isRefreshToken = false;
          });
      } else {
        // 正在刷新token,返回一个未执行resolve的promise
        return new Promise(resolve => {
          // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
          retryRequest.push(token => {
            config.headers['Authorization'] = token;
            return resolve(http(config));
          });
        });
      }
    } else {
      if (error.code !== 'ERR_CANCELED') {
        // 错误拦截,排除自行取消接口的错误类型
        $uiMsg.error(`网络错误,请稍后再试,错误代码:${error.response && error.response.status ? error.response.status : ''}`);
      }
      return Promise.reject(error);
    }
  }
);

/*  封装get请求
    url:请求地址
    params:请求参数
    config:请求配置(请求开始时是否展示loading,请求完成后是否隐藏loading,请求错误时是否显示错误信息)
*/
const get = (url, params, config = { showLoading: true, hideLoading: true, showError: true }) => {
  if (config.showLoading) {
    ShowLoading();
    loadingStore.Show();
  }
  return new Promise((resolve, reject) => {
    // 当请求参数为对象时,过滤掉值为null的参数
    let _params = Object.prototype.toString.call(params) === '[object Object]' ? _.pickBy(params, item => !_.isNil(item)) : params;
    http
      .get(url, _params)
      .then(res => {
        (config.hideLoading || config.hideLoading === undefined) && HideLoading();
        if (res?.code === 200 || url.indexOf('/api/oss/sign') !== -1) {
          resolve(res);
        } else {
          if ((config.showError || config.showError === undefined) && res.msg) {
            $uiMsg.error(res.msg, () => {
              reject(res);
            });
          } else {
            reject(res);
          }
        }
      })
      .catch(error => {
        if (error.code !== 'ERR_CANCELED') {
          reject(error);
        }
      });
  });
};

// 封装post请求,参数同get,filterNull:是否需要过滤掉值为null的参数
const post = (url, params, config = { showLoading: true, hideLoading: true, showError: true, filterNull: true }) => {
  if (config.showLoading) {
    ShowLoading();
    loadingStore.Show();
  }
  let _filterNull = config.filterNull === undefined ? true : config.filterNull;
  return new Promise((resolve, reject) => {
    let _params = params;
    if (_filterNull) {
      _params = Object.prototype.toString.call(params) === '[object Object]' ? _.pickBy(params, item => !_.isNil(item)) : params;
    }
    http
      .post(url, _params)
      .then(res => {
        (config.hideLoading || config.hideLoading === undefined) && HideLoading();
        if (res?.code === 200) {
          resolve(res);
        } else {
          if ((config.showError || config.showError === undefined) && res.msg) {
            $uiMsg.error(res.msg, () => {
              reject(res);
            });
          } else {
            reject(res);
          }
        }
      })
      .catch(error => {
        if (error.code !== 'ERR_CANCELED') {
          reject(error);
        }
      });
  });
};

export { get, post };

补上另外几个组件的代码

pinia.js

import { createPinia } from 'pinia';

const pinia = createPinia();

export default pinia;

loading.js 公共loading状态store

import { defineStore } from 'pinia';

export const useLoadingStore = defineStore('loadingStore', {
  state: () => ({
    isLoading: false
  }),
  actions: {
    Show() {
      this.isLoading = true;
    },
    Hide() {
      this.isLoading = false;
    }
  }
});

user.js 用户信息store

import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: JSON.parse(localStorage.getItem('userInfo')),
    token: JSON.parse(localStorage.getItem('token'))
  }),
  actions: {
    LoginIn(data) {
      this.token = data.token;
      this.userInfo = data;
      localStorage.setItem('userInfo', JSON.stringify(data));
      localStorage.setItem('token', JSON.stringify(data.token));
      localStorage.setItem('expire', data.expire);
    },
    LoginOut() {
      localStorage.removeItem('userInfo');
      localStorage.removeItem('token');
      localStorage.removeItem('expire');
      location.href = '/';
    }
  }
});

request.js 请求队列store

import { defineStore } from 'pinia';

export const useRequestStore = defineStore('requestStore', {
  state: () => ({
    list: []
  }),
  actions: {
    Add(data) {
      this.list.push(data);
    },
    Del(index) {
      this.list.splice(index, 1);
    }
  }
});

router.js

import { useRequestStore } from '../store/request';
import pinia from './pinia';

const requestStore = useRequestStore(pinia);

router.beforeEach((to, from, next) => {
  // 切换router时,取消pending中的请求
  if (requestStore.list.length) {
    requestStore.list.forEach((item, index) => {
      item.cancel();
      requestStore.Del(index);
    });
  }
})
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容