Skip to content

axios封装

功能列表

  • 1、全局loading
  • 2、自动防止重复提交
  • 3、页面跳转时,自动取消还未完成的请求
  • 4、登录信息过期,用户无感知刷新登录状态
  • 5、全局拦截器,错误提示等等
  • 6、快捷调用方式

全局loading

  • 1、全局定义一个loadingCount用来记录请求的次数,默认是0
  • 2、当有用户调用接口时,如果loadingCount时0,开启loading动画,并且loadingCount累加
  • 3、当接口请求成功、或者接口异常时,loadingCount递减,当loadingCount等于0时,说明所以接口已经请求完毕,需要关闭loading动画

自动防止重复提交

  • 1、全局定义一个Map,用来记录接口生成的标记
  • 2、当有用户调用接口时,根据接口的地址、请求的方式、参数,生成一个字符串
  • 3、在接口发送之前在request.use进行拦截,如果Map对象中已经存在此标记,说明上一次请求还未结束,抛出一自定义异常来终止这次的请求
  • 4、当接口请求成功、或者接口异常时需要从Map对象中删除接口标记

页面跳转时,自动取消还未完成的请求

  • 1、当有用户调用接口时在request.use进行拦截,创建一个axios.CancelToken,把需要取消的请求,存储vuex在队列中
  • 2、Vue当页面发生跳转时,首先会进入beforeEach钩子函数,从vuex取出CancelToken,取消未完成的请求

登录信息过期,用户无感知刷新登录状态

  • 1、首先我们是采用的JWT的方式,用来记录用户的状态,在登录的时候会accessToken过期时间是几个小时,而另外一个refreshToken用来刷新accessToken
  • 2、预先和服务端定义好,当用户状态过期时,请求接口的时候返回一个0200002的code码
  • 3、当前端在response.use中得知状态码是0200002,首先用户通过refreshToken刷新
  • 4、刷新成功后,直接在继续通过上下文对象请求完成接口调用

解决并发登录信息过期问题

  • 1、全局定义一个waitToken标志位默认为false
  • 2、当拦截到接口登录信息过期时,如果waitToken标志位false时,将标志位改为true并且去换取新的accessToken
  • 3、否则创建一个Promise对象,通过subscribe订阅器resolve(this.request(res.config))收集起来
  • 4、当accessToken换取之后,通过dispatch出发执行订阅的函数执行resolve,并且将收集器清空
ts
import axios from 'axios';
import type {
  AxiosInstance,
  AxiosRequestConfig,
  Canceler,
  AxiosResponse,
} from 'axios';
import { showLoadingToast, closeToast, showFailToast } from 'vant';
import { refreshToken } from './../apis';

import { store } from './../store';
import { I18nMessages, Typelanguage } from '../locales';
import pubSub from '.';

interface Params {
  [key: string]: unknown;
}

interface IAxiosHelper {
  isLoading: boolean;
  isToast: boolean;
  isCancelReq: boolean;
  isDeduplication: boolean;
  reqUniqueKey: string;
}

const { subscribe, dispatch } = pubSub();

axios.defaults.headers.common['Content-Type'] =
  'application/json;charset=UTF-8';
class Httprequest {
  private timeout: number;
  private queue: Params;
  private loadingCount: number;
  private waitToken: boolean; // 是否正在换取新的Token
  constructor() {
    this.timeout = 50000; // 请求的超时时间50秒
    this.queue = {}; // 请求队列,防止重复点击
    this.loadingCount = 0;
    this.waitToken = false;
  }

  _closeLoading(isLoading: boolean) {
    if (isLoading) {
      this.loadingCount--;
      if (this.loadingCount <= 0) {
        closeToast();
        this.loadingCount = 0;
      }
    }
  }

  setinterceptors(
    instance: AxiosInstance,
    {
      isLoading,
      isToast,
      isCancelReq,
      isDeduplication,
      reqUniqueKey,
    }: IAxiosHelper,
  ) {
    const { processing, serverException } =
      I18nMessages[store.state.language.type as Typelanguage].http;
    instance.interceptors.request.use((config) => {
      const token: string = store.state.user.token;
      if (config.headers) {
        if (token) {
          config.headers['X-Token'] = token;
        }
      }

      if (isDeduplication) {
        if (this.queue[reqUniqueKey]) {
          throw 'EXCEPTION:REPEATCLICK';
        } else {
          this.queue[reqUniqueKey] = reqUniqueKey;
        }
      }

      if (isLoading) {
        this.loadingCount++;
        showLoadingToast({
          duration: 0,
          forbidClick: true,
          message: 'Loading...',
        });
      }
      if (isCancelReq) {
        config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
          store.dispatch(`cancelReq/enqueue`, cancel);
        });
      }

      return config;
    });

    instance.interceptors.response.use(
      (res: AxiosResponse) => {
        delete this.queue[reqUniqueKey];
        this._closeLoading(isLoading);
        if (Number(res.data.code) === 0) {
          return Promise.resolve(res.data.data);
        } else if (res.data.code === '0200002') {
          console.log('登录过期');
          // 登录过期
          const refreshTokenApi = new Promise<void>((resolve, reject) => {
            if (!this.waitToken) {
              this.waitToken = true;
              // 换取刷新Token
              store.dispatch('user/updateUserInfo', {
                token: store.state.user.refreshToken,
              });
              refreshToken()
                .then((tokenRes) => {
                  const token = tokenRes.token;
                  if (token) {
                    // 更新用户token&refreshToken
                    store.dispatch('user/updateUserInfo', {
                      token: token,
                      refreshToken: tokenRes.refreshToken,
                    });
                    resolve();
                    this.waitToken = false;
                    dispatch();
                  } else {
                    console.log('重新登录');
                    reject();
                    this.waitToken = false;
                    dispatch();
                  }
                })
                .catch(() => {
                  reject();
                  this.waitToken = false;
                  dispatch();
                });
            } else {
              console.log('正在换取Token,请等待', this.waitToken);
              return new Promise((resolve) => {
                subscribe(() => {
                  setTimeout(() => resolve(this.request(res.config)));
                });
              });
            }
          });
          return refreshTokenApi.then(() => this.request(res.config));
        }
        if (isToast) {
          showFailToast(res.data.message);
        }
        return Promise.reject(res.data);
      },
      (error) => {
        this._closeLoading(isLoading);
        if (error === 'EXCEPTION:REPEATCLICK') {
          showFailToast(processing);
        } else if (error.__proto__.constructor.name === 'Cancel') {
          delete this.queue[reqUniqueKey];
          console.log('请求取消');
        } else {
          delete this.queue[reqUniqueKey];
          // 请求取消不处理
          if (error.message !== 'canceled') {
            showFailToast(serverException);
          }
        }

        return Promise.reject(error.message || error);
      },
    );
  }
  /**
   *
   * @param {*} axiosConfig axios 参数
   * @param {*} isLoading 开启loading
   * @param {*} isToast 开启接口异常提示
   * @param {*} isCancelReq 取消上一个页面未完成的请求
   * @param {*} isDeduplication 开启接口防重
   * @returns
   */
  request<T>(
    axiosConfig: AxiosRequestConfig,
    {
      isLoading = true,
      isToast = true,
      isCancelReq = true,
      isDeduplication = true,
    }: Partial<IAxiosHelper> = {},
  ): Promise<T> {
    const instance: AxiosInstance = axios.create();
    axiosConfig.method || (axiosConfig.method = 'post');
    const config = {
      baseURL: import.meta.env.VITE_API_HOST,
      timeout: this.timeout,
      ...axiosConfig,
    };
    let reqUniqueKey = '';
    if (isDeduplication) {
      let paramsStr = '';
      if (axiosConfig.method.toLocaleUpperCase() === 'GET') {
        paramsStr = JSON.stringify(config.params || {});
      } else {
        paramsStr = JSON.stringify(config.data || {});
      }
      // 确定唯一性(路由+接口地址+请求方式)
      reqUniqueKey =
        `${location.href}${config.method}${config.url}${paramsStr}`.toLocaleUpperCase();
    }

    this.setinterceptors(instance, {
      isLoading,
      isToast,
      isCancelReq,
      isDeduplication,
      reqUniqueKey,
    });
    return instance(config);
  }
}

const http = new Httprequest();

export const httpGet = <T>(
  url: string,
  params?: Params,
  axiosHelper?: Partial<IAxiosHelper>,
): Promise<T> => {
  return http.request<T>(
    {
      url,
      params,
      method: 'GET',
    },
    axiosHelper,
  );
};
export const httpPost = <T>(
  url: string,
  data?: Params,
  axiosHelper?: Partial<IAxiosHelper>,
): Promise<T> => {
  return http.request<T>(
    {
      url,
      data,
    },
    axiosHelper,
  );
};

export default http.request.bind(http);
ts
import { UserInfo } from '../store/user';
import { httpGet, httpPost } from '../utils/Httprequest';

export const loginApi = (data: {
  phone: string;
  code: string;
}): Promise<UserInfo> => {
  return httpPost('/user/login', data);
};

// 刷新Token
export const refreshToken = (): Promise<{
  token: string;
  refreshToken: string;
}> => {
  return httpGet(
    '/user/refresh',
    {},
    {
      isLoading: false,
      isToast: false,
      isCancelReq: false,
      isDeduplication: false,
    },
  );
};