这个方案其实是当年我后端转前端的时候,架构师带团队的时候给我们讲的,那时候还是个小白,promise 都不知道是什么,今天正好有空,整理一下;
实现思路
-
对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次,而是对于相同的请求先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求;
-
思路比较明确,注意一下几个点:
- 在拿到响应结果后,返回给之前挂起的请求时,要用到发布订阅模式;
- 对于挂起的请求,需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过
return Promise.reject()
来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理; - 如果是附件上传接口其实一般都不会重复请求多次,对大的附件进行hash计算比较耗时,这里暂时没有处理,可以直接放行;
const isFileUploadApi = config => Object.prototype.toString.call(config.data) === "[object FormData]";
代码实现
-
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); }
-
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); } }); } } }
-
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("&");
-
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>
-
演示:
源码下载
React✍️ 工具
上一篇