美文网首页
用在后台管理系统的一个公共请求组件,解决重复换取token,过滤

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

作者: Zick | 来源:发表于2023-04-24 16:34 被阅读0次

    后台管理系统的技术栈: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);
        });
      }
    })
    

    相关文章

      网友评论

          本文标题:用在后台管理系统的一个公共请求组件,解决重复换取token,过滤

          本文链接:https://www.haomeiwen.com/subject/fzbujdtx.html