封装axios,实现重复请求共享结果

封装axios

  1. 可以实现多域名接口

  2. 取消重复请求

  3. 取消的请求共享正确请求的返回结果

    *A请求和B请求重复, 先发送A请求,再发送B请求, 因为B请求和A请求一样,则取消B请求,B请求返回的是A请求的返回结果*

    3.1 当取消请求响应时,正确请求的结果还未返回时, 通过发布订阅模式订阅回调函数, 当正确请求的结果返回时发布,触发回调函数的执行,返回结果通过回调函数参数传递,再通过Promise.resolve将结果返回;

    5.2 当取消请求响应时,正确请求的结果已经返回,则在正确请求结果返回时,将结果缓存,在取消请求响应时,判断缓存中是否有结果,有则直接返回。

  4. 添加公用header

  5. 统一处理未登录,无权限接口

js 复制代码
import axios from 'axios';
import { netConfig } from '@/config/net.config';
import EventEmitter from './EventEmitter';
import md5 from 'md5';
const { contentType, invalidCode, noPermissionCode, requestTimeout } = netConfig;
import router from '@/router/index.js';
import { ElMessageBox } from 'element-plus';
import qs from 'qs';
import { getCurrAppId } from '@/common/project';
import { getDataSource, setDataSource } from './cookies';

const BaseUrlOP = import.meta.env.VITE_APP_WEB_URL;
const BaseUrl = import.meta.env.VITE_APP_BASE_URL;
const BaseUrlSG = import.meta.env.VITE_APP_BASE_URL_SG;
const RunnerUrl = import.meta.env.VITE_APP_RUNNER_URL;
const QaPlatUrl = import.meta.env.VITE_APP_PLATQA_URL;

const eventEmitter = new EventEmitter();

// eslint-disable-next-line no-unused-vars
let tokenLose = true;

/**
 *
 * @description 处理code异常
 * @param {*} code
 * @param msg
 */
const handleCode = (code, msg) => {
  switch (code) {
    case invalidCode:
      tokenLose = false;
      ElMessageBox.confirm(msg, '重新登录', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(async () => {
          // 处理重新登录逻辑
        })
        .catch(() => {
          tokenLose = true;
        });
      break;
    case noPermissionCode:
      router.push({ path: '/401' }).catch(() => {});
      break;
    default:
      break;
  }
};

// reqList数组存储时间段内的请求
const reqList = [];
// 阻止重复请求; 如果某一段时间内请求重复,则只保留第一个请求,取消其他请求
const stopRepeatRequest = (config, cancel, errorMessage) => {
  const errorMsg = errorMessage || '';
  for (let i = 0; i < reqList.length; i++) {
    if (
      reqList[i].url === config.url &&
      reqList[i].method === config.method &&
      reqList[i].data === JSON.stringify(config.data)
    ) {
      // url、method、请求参数,三者md5加密,生成唯一hash
      let hash = md5(config.url + config.method + JSON.stringify(config.data));
      cancel(errorMsg + ':' + hash);
      return;
    }
  }
  reqList.push({
    url: config.url,
    method: config.method,
    data: JSON.stringify(config.data),
  });
};

// 允许某个请求可以继续进行
const allowRequest = (config) => {
  for (let i = 0; i < reqList.length; i++) {
    if (
      reqList[i].url === config.url &&
      reqList[i].method === config.method &&
      reqList[i].data === config.data
    ) {
      let hash = md5(config.url + config.method + JSON.stringify(config.data));
      cacheResponse[hash]; // 清除缓存
      reqList.splice(i, 1);
      break;
    }
  }
};

// cacheResponse对象缓存请求结果,
const cacheResponse = {};
// 创建axios实例
const instance = axios.create({
  timeout: requestTimeout,
  headers: {
    'Content-Type': contentType,
  },
});

// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    if (config.url.includes('/aaa/***')) {
      config.baseURL = BaseUrlOP;
    } else if (config.url.includes('/bbb/***')) {
      config.baseURL = RunnerUrl;
    } else if (
      config.url.startsWith('/ccc/***') ||
      config.url.startsWith('/ddd/***')
    ) {
      config.baseURL = QaPlatUrl;
    } else {
      config.headers['App-Id'] = getCurrAppId();
      let url = '?' + window.location.href.split('?')[1];
      const urlSearchParems = new URLSearchParams(url);
      const params = Object.fromEntries(urlSearchParems.entries());
      if (params && params.env === 'os') {
        config.baseURL = BaseUrlSG;
        setDataSource('out');
      } else if (params && params.env === 'main') {
        config.baseURL = BaseUrl;
        setDataSource('in');
      } else {
        if (getDataSource() === 'out') {
          config.baseURL = BaseUrlSG;
        } else {
          config.baseURL = BaseUrl;
        }
      }
    }
    config.url = config.url.replaceAll('//', '/');
    if (
      config.data &&
      config.headers['Content-Type'] === 'application/x-www-form-urlencoded;charset=UTF-8'
    )
      config.data = qs.stringify(config.data);
    config.withCredentials = true;
    instance.defaults.retryDelay = 1000;
    // config.crossDomain = true;

    let cancel;
    config.cancelToken = new axios.CancelToken(function (c) {
      cancel = c;
    });
    stopRepeatRequest(config, cancel, `${config.url}请求被中断`);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

instance.interceptors.response.use(
  (response) => {
    /// 增加延迟,相同的请求不得在短时间内重复发送
    setTimeout(() => {
      allowRequest(response.config);
    }, 1000);
    let config = response.config;
    let hash = md5(config.url + config.method + JSON.stringify(config.data));
    cacheResponse[hash] = response; // 发布时还没有订阅; 将结果缓存
    eventEmitter.emit(hash, response); // 订阅发生在发布之前
    console.log('emit', config.url, hash, response, eventEmitter.subscribers);
    return response;
  },
  async (error) => {
    if (axios.isCancel(error)) {
      let hash = error.message.split(':')[1];
      if (cacheResponse[hash]) return cacheResponse[hash]; // 未取消请求已经返回了结果,直接使用该缓存结果
      let p = await new Promise((resolve) => {
        // 订阅必须在发布之前
        eventEmitter.subscribe(hash, (res) => {
          resolve(res);
        });
      });
      return p;
    } else {
      /// 增加延迟,相同的请求不得在短时间内重复发送
      setTimeout(() => {
        allowRequest(error.config);
      }, 1000);
      if (error.response) {
        const res = error.response.data;
        if (res.data) {
          const { data } = res;
          const { retcode, message } = data;
          handleCode(retcode, message);
        }
      }
      return Promise.reject(error);
    }
  }
);

export default instance;

发布订阅EventEmitter的实现

js 复制代码
class EventEmitter {
  constructor() {
    this.subscribers = {};
  }
  // 订阅
  subscribe(eventName, callback) {
    if (!this.subscribers[eventName]) {
      this.subscribers[eventName] = [];
    }
    this.subscribers[eventName].push(callback);
  }
  // 取消订阅
  unsubscribe(eventName, callback) {
    if (this.subscribers[eventName]) {
      this.subscribers[eventName] = this.subscribers[eventName].filter((cb) => cb != callback);
    }
  }
  // 发布
  emit(eventName, ...args) {
    if (this.subscribers[eventName])
      this.subscribers[eventName].forEach((callback) => {
        callback.apply(null, args);
      });
  }
}

export default EventEmitter;
相关推荐
光影少年1 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
mosen8682 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
Gavin_9153 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
Devil枫9 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
GIS程序媛—椰子10 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
毕业设计制作和分享11 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
程序媛小果11 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
从兄12 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript