这个方案其实是当年我后端转前端的时候,架构师带团队的时候给我们讲的,那时候还是个小白,promise 都不知道是什么,今天正好有空,整理一下;

实现思路

  1. 对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次,而是对于相同的请求先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求;

  2. 思路比较明确,注意一下几个点:

    1. 在拿到响应结果后,返回给之前挂起的请求时,要用到发布订阅模式;
    2. 对于挂起的请求,需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过 return Promise.reject() 来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理;
    3. 如果是附件上传接口其实一般都不会重复请求多次,对大的附件进行hash计算比较耗时,这里暂时没有处理,可以直接放行;
      const isFileUploadApi = config => Object.prototype.toString.call(config.data) === "[object FormData]";
      

代码实现

  1. request.ts

    import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
    import { EventEmitter } from "./eventEmitter";
    import { generateReqKey } from "./generateReqKey";
    const baseURL = (window as any).globalConstant.baseUrl;
    
    export const axiosInstance = axios.create({
      baseURL: baseURL,
      timeout: 30000,
      withCredentials: false, // 跨域请求是否携带cookie
    });
    
    // 存储已发送但未响应的请求
    const pendingRequest = new Set();
    // 发布订阅容器
    const eventEmitter = new EventEmitter();
    
    // 拦截请求
    axiosInstance.interceptors.request.use(
      async (config: AxiosRequestConfig) => {
        // 生成请求Key
        let reqKey = generateReqKey(config, location.hash);
        if (pendingRequest.has(reqKey)) {
          // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
          // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
          let res = null;
          try {
            // 接口成功响应
            res = await new Promise((resolve, reject) => {
              eventEmitter.on(reqKey, resolve, reject);
            });
            return Promise.reject({ type: "limiteResSuccess", val: res });
          } catch (limitFunErr) {
            // 接口报错
            return Promise.reject({ type: "limiteResError", val: limitFunErr });
          }
        } else {
          // 将请求的key保存在config
          config.pendKey = reqKey;
          pendingRequest.add(reqKey);
        }
    
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
    
    // 拦截响应
    axiosInstance.interceptors.response.use(
      (response: AxiosResponse) => {
        handleSuccessResponse_limit(response); // 将拿到的结果发布给其他相同的接口
        return response;
      },
      (error) => {
        return handleErrorResponse_limit(error);
      }
    );
    
    // 接口响应成功
    function handleSuccessResponse_limit(response) {
      const reqKey = response.config.pendKey;
      if (pendingRequest.has(reqKey)) {
        let x = null;
        try {
          x = JSON.parse(JSON.stringify(response));
        } catch (e) {
          x = response;
        }
        pendingRequest.delete(reqKey);
        eventEmitter.emit(reqKey, x, "resolve");
        delete eventEmitter.reqKey;
      }
    }
    
    // 接口走失败响应
    function handleErrorResponse_limit(error) {
      if (error.type && error.type === "limiteResSuccess") {
        return Promise.resolve(error.val);
      } else if (error.type && error.type === "limiteResError") {
        return Promise.reject(error.val);
      } else {
        const reqKey = error.config.pendKey;
        if (pendingRequest.has(reqKey)) {
          let x = null;
          try {
            x = JSON.parse(JSON.stringify(error));
          } catch (e) {
            x = error;
          }
          pendingRequest.delete(reqKey);
          eventEmitter.emit(reqKey, x, "reject");
          delete eventEmitter.reqKey;
        }
      }
      return Promise.reject(error);
    }
    
  2. eventEmitter.ts

    type EventType = Record<string, any[]>;
    
    export class EventEmitter {
      event: EventType;
    
      constructor() {
        this.event = {};
      }
    
      on(type, resolve, reject) {
        if (!this.event[type]) {
          this.event[type] = [[resolve, reject]];
        } else {
          this.event[type].push([resolve, reject]);
        }
      }
    
      emit(type, res, ansType) {
        if (!this.event[type]) return;
        else {
          this.event[type].forEach((cbArr) => {
            if (ansType === "resolve") {
              cbArr[0](res);
            } else {
              cbArr[1](res);
            }
          });
        }
      }
    }
    
  3. generateReqKey.ts

    import { AxiosRequestConfig } from "axios";
    
    // 这里偷懒,没有使用 hash,使用的字符串拼接
    export const generateReqKey = ({ method, url, params, data }: AxiosRequestConfig, hash?: string) => [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
    
  4. test.vue

    <template>
        <GlobalContentTitle />
        <div class="host-container">
            <el-button class="btn" type="primary" @click="getJson">获取数据</el-button>
        </div>
    </template>
    <script setup lang='ts'>
    import { axiosInstance } from '@/utils/request';
    import { ElMessage } from 'element-plus';
    
    const getJson = async () => {
        axiosInstance.post(`http://192.168.9.61:40066/msg/approval/auditManagement/approvalFlow`, { "pageNum": 1, "pageSize": 10 }, {
            headers: {
                'Content-Type': 'application/json',
                'dsjrz-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'dsjrz-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
                'rzzx-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'rzzx-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
            }
        }).then((res: any) => {
            ElMessage.success(JSON.stringify(res));
        }).catch((err: any) => {
            console.error(err);
        });
    
        axiosInstance.post(`http://192.168.9.61:40066/msg/approval/auditManagement/approvalFlow`, { "pageNum": 1, "pageSize": 10 }, {
            headers: {
                'Content-Type': 'application/json',
                'dsjrz-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'dsjrz-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
                'rzzx-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'rzzx-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
            }
        }).then((res: any) => {
            ElMessage.success(JSON.stringify(res));
        }).catch((err: any) => {
            console.error(err);
        });
        
        axiosInstance.post(`http://192.168.9.61:40066/msg/approval/auditManagement/approvalFlow`, { "pageNum": 1, "pageSize": 10 }, {
            headers: {
                'Content-Type': 'application/json',
                'dsjrz-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'dsjrz-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
                'rzzx-apptoken': 'ydzy_glc.14b6347c1698467996dbc85fbc8acfce',
                'rzzx-usertoken': 'ydzy_glc.3283a8bc62c740e9a49d54ebd1d99111',
            }
        }).then((res: any) => {
            ElMessage.success(JSON.stringify(res));
        }).catch((err: any) => {
            console.error(err);
        });
    };
    </script>
    
    <style scoped lang='scss'>
    .host-container {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        background: #fff;
    }
    </style>
    
  5. 演示:

源码下载

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 实现思路
  2. 2. 代码实现
  3. 3. 源码下载