目录

前端如何彻底解决重复请求问题

背景

  1. 保存按钮点击多次,造成新增多个单据
  2. 列表页疯狂刷新,导致服务器压力大

如何彻底解决

方案:我的思路从请求层面判断相同请求只发送一次,将结果派发给各个订阅者

实现思路

  1. 对请求进行数据进行hash
  2. 添加store 存储 hash => Array promise
  3. 相同请求,直接订阅对应的promise
  4. 请求取消,则将store中对应的promise置为null
  5. 请求返回后,调用所有未取消的订阅

核心代码

typescirpt 复制代码
  private handleFinish(key: string, index: number) {
    const promises = this.store.get(key);
    // 只有一个promise时则删除store
    if (promises?.filter((item) => item).length === 1) {
      this.store.delete(key);
    } else if (promises && promises[index]) {
    // 还有其他请求,则将当前取消的、或者完成的置为null
      promises[index] = null;
    }
  }

  private async handleRequest(config: any) {
    const hash = sha256.create();
    hash.update(
      JSON.stringify({
        params: config.params,
        data: config.data,
        url: config.url,
        method: config.method,
      }),
    );
    const fetchKey = hash.hex().slice(0, 40);
    const promises = this.store.get(fetchKey);
    const index = promises?.length || 0;
    let promise = promises?.find((item) => item);
    const controller = new AbortController();

    if (config.signal) {
      config.signal.onabort = (reason: any) => {
        const _promises = this.store.get(fetchKey)?.filter((item) => item);
        if (_promises?.length === 1) {
          controller.abort(reason);
          this.handleFinish(fetchKey, index);
        }
      };
    }

    if (!promise) {
      promise = this.instance({
        ...config,
        signal: controller.signal,
        headers: {
          ...config.headers,
          fetchKey,
        },
      }).catch((error) => {
        console.log(error, "请求错误");
        // 失败的话,立即删除,可以重试
        this.handleFinish(fetchKey, index);
        return { error };
      });
    }

    const newPromise = Promise.resolve(promise)
      .then((result: any) => {
        if (config.signal?.aborted) {
          this.handleFinish(fetchKey, index);
          return result;
        }
        return result;
      })
      .finally(() => {
        setTimeout(() => {
          this.handleFinish(fetchKey, index);
        }, 500);
      });
    this.store.set(fetchKey, [...(promises || []), newPromise]);
    return newPromise;
  }

以下为完整代码(仅供参考)

index.ts

typescript 复制代码
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { sha256 } from "js-sha256";
import transformResponseValue, { updateObjTimeToUtc } from "./utils";

type ErrorInfo = {
  message: string;
  status?: number;
  traceId?: string;
  version?: number;
};

type MyAxiosOptions = AxiosRequestConfig & {
  goLogin: (type?: string) => void;
  onerror: (info: ErrorInfo) => void;
  getHeader: () => any;
};

export type MyRequestConfigs = AxiosRequestConfig & {
  // 是否直接返回服务端返回的数据,默认false, 只返回 data
  useOriginData?: boolean;
  // 触发立即更新
  flushApiHook?: boolean;
  ifHandleError?: boolean;
};

type RequestResult<T, U> = U extends { useOriginData: true }
  ? T
  : T extends { data?: infer D }
    ? D
    : never;

class LmAxios {
  private instance: AxiosInstance;

  private store: Map<string, Array<Promise<any> | null>>;

  private options: MyAxiosOptions;

  constructor(options: MyAxiosOptions) {
    this.instance = axios.create(options);

    this.options = options;
    this.store = new Map();
    this.interceptorRequest();
    this.interceptorResponse();
  }

  // 统一处理为utcTime
  private interceptorRequest() {
    this.instance.interceptors.request.use(
      (config) => {
        if (config.params) {
          config.params = updateObjTimeToUtc(config.params);
        }
        if (config.data) {
          config.data = updateObjTimeToUtc(config.data);
        }
        return config;
      },
      (error) => {
        console.log("intercept request error", error);
        Promise.reject(error);
      },
    );
  }

  // 统一处理为utcTime
  private interceptorResponse() {
    this.instance.interceptors.response.use(
      (response): any => {
        // 对响应数据做处理,以下根据实际数据结构改动!!...
        const [checked, errorInfo] = this.checkStatus(response);

        if (!checked) {
          return Promise.reject(errorInfo);
        }

        const disposition =
          response.headers["content-disposition"] ||
          response.headers["Content-Disposition"];
        // 文件处理
        if (disposition && disposition.indexOf("attachment") !== -1) {
          const filenameReg =
            /filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/g;
          const filenames: string[] = [];
          disposition.replace(filenameReg, (r: any, r1: string) => {
            filenames.push(decodeURIComponent(r1));
          });
          return Promise.resolve({
            filename: filenames[filenames.length - 1],
            data: response.data,
          });
        }
        if (response) {
          return Promise.resolve(response.data);
        }
      },
      (error) => {
        console.log("request error", error);
        if (error.message.indexOf("timeout") !== -1) {
          return Promise.reject({
            message: "请求超时",
          });
        }
        const [checked, errorInfo] = this.checkStatus(error.response);
        return Promise.reject(errorInfo);
      },
    );
  }

  private checkStatus(
    response: AxiosResponse<any>,
  ): [boolean] | [boolean, ErrorInfo] {
    const { code, message = "" } = response?.data || {};
    const { headers, status } = response || {};

    if (!status) {
      return [false];
    }

    // 单地登录判断,弹出不同提示
    if (status === 401) {
      this.options?.goLogin();
      return [false];
    }

    if (code === "ECONNABORTED" && message?.indexOf("timeout") !== -1) {
      return [
        false,
        {
          message: "请求超时",
        },
      ];
    }

    if ([108, 109, 401].includes(code)) {
      this.options.goLogin();
      return [false];
    }
    if ((code >= 200 && code < 300) || code === 304) {
      // 如果http状态码正常,则直接返回数据
      return [true];
    }

    if (!code && ((status >= 200 && status < 300) || status === 304)) {
      return [true];
    }

    let errorInfo = "";
    const _code = code || status;
    switch (_code) {
      case -1:
        errorInfo = "远程服务响应失败,请稍后重试";
        break;
      case 400:
        errorInfo = "400: 错误请求";
        break;
      case 401:
        errorInfo = "401: 访问令牌无效或已过期";
        break;
      case 403:
        errorInfo = message || "403: 拒绝访问";
        break;
      case 404:
        errorInfo = "404: 资源不存在";
        break;
      case 405:
        errorInfo = "405: 请求方法未允许";
        break;
      case 408:
        errorInfo = "408: 请求超时";
        break;
      case 500:
        errorInfo = message || "500: 访问服务失败";
        break;
      case 501:
        errorInfo = "501: 未实现";
        break;
      case 502:
        errorInfo = "502: 无效网关";
        break;
      case 503:
        errorInfo = "503: 服务不可用";
        break;
      default:
        errorInfo = "连接错误";
    }

    return [
      false,
      {
        message: errorInfo,
        status: _code,
        traceId: response?.data?.requestId,
        version: response.data.ver,
      },
    ];
  }

  private handleFinish(key: string, index: number) {
    const promises = this.store.get(key);
    if (promises?.filter((item) => item).length === 1) {
      this.store.delete(key);
    } else if (promises && promises[index]) {
      promises[index] = null;
    }
  }

  private async handleRequest(config: any) {
    const hash = sha256.create();
    hash.update(
      JSON.stringify({
        params: config.params,
        data: config.data,
        url: config.url,
        method: config.method,
      }),
    );
    const fetchKey = hash.hex().slice(0, 40);
    const promises = this.store.get(fetchKey);
    const index = promises?.length || 0;
    let promise = promises?.find((item) => item);
    const controller = new AbortController();

    if (config.signal) {
      config.signal.onabort = (reason: any) => {
        const _promises = this.store.get(fetchKey)?.filter((item) => item);
        if (_promises?.length === 1) {
          controller.abort(reason);
          this.handleFinish(fetchKey, index);
        }
      };
    }

    if (!promise) {
      promise = this.instance({
        ...config,
        signal: controller.signal,
        headers: {
          ...config.headers,
          fetchKey,
        },
      }).catch((error) => {
        console.log(error, "请求错误");
        // 失败的话,立即删除,可以重试
        this.handleFinish(fetchKey, index);
        return { error };
      });
    }

    const newPromise = Promise.resolve(promise)
      .then((result: any) => {
        if (config.signal?.aborted) {
          this.handleFinish(fetchKey, index);
          return result;
        }
        return result;
      })
      .finally(() => {
        setTimeout(() => {
          this.handleFinish(fetchKey, index);
        }, 500);
      });
    this.store.set(fetchKey, [...(promises || []), newPromise]);
    return newPromise;
  }

  // add override type
  public async request<T = unknown, U extends MyRequestConfigs = {}>(
    url: string,
    config: U,
  ): Promise<RequestResult<T, U> | null> {
    // todo
    const options = {
      url,
      // 是否统一处理接口失败(提示)
      ifHandleError: true,
      ...config,
      headers: {
        ...this.options.getHeader(),
        ...config?.headers,
      },
    };

    const res = await this.handleRequest(options);

    if (!res) {
      return null;
    }

    if (res.error) {
      if (res.error.message && options.ifHandleError) {
        this.options.onerror(res.error);
      }
      throw new Error(res.error);
    }

    if (config.useOriginData) {
      return res;
    }

    if (config.headers?.feTraceId) {
      window.dispatchEvent(
        new CustomEvent<{ flush?: boolean }>(config.headers.feTraceId, {
          detail: {
            flush: config?.flushApiHook,
          },
        }),
      );
    }

    // 默认返回res.data
    return transformResponseValue(res.data)
  }
}

export type MyRequest = <T = unknown, U extends MyRequestConfigs = {}>(
  url: string,
  config: U,
) => Promise<RequestResult<T, U> | null>;

export default LmAxios;

utils.ts(这里主要用来处理utc时间,你可能用不到删除相关代码就好)

typescript 复制代码
import moment from 'moment';

const timeReg =
  /^\d{4}([/:-])(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)($|( |T)(?:[01]\d|2[0-3])(:[0-5]\d)?(:[0-5]\d)?(\..*\d)?Z?$)/;

export function formatTimeValue(time: string, format = 'YYYY-MM-DD HH:mm:ss') {
  if (typeof time === 'string' || typeof time === 'number') {
    if (timeReg.test(time)) {
      return moment(time).format(format);
    }
  }
  return time;
}

// 统一转化如参
export const updateObjTimeToUtc = (obj: any) => {
  if (typeof obj === 'string') {
    if (timeReg.test(obj)) {
      return moment(obj).utc().format();
    }
    return obj;
  }
  if (toString.call(obj) === '[object Object]') {
    const newObj: Record<string, any> = {};
    Object.keys(obj).forEach((key) => {
      newObj[key] = updateObjTimeToUtc(obj[key]);
    });
    return newObj;
  }
  if (toString.call(obj) === '[object Array]') {
    obj = obj.map((item: any) => updateObjTimeToUtc(item));
  }
  return obj;
};

const utcReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/;

const transformResponseValue = (res: any) => {
  if (!res) {
    return res;
  }
  if (typeof res === 'string') {
    if (utcReg.test(res)) {
      return moment(res).format('YYYY-MM-DD HH:mm:ss');
    }
    return res;
  }
  if (toString.call(res) === '[object Object]') {
    const result: any = {};
    Object.keys(res).forEach((key) => {
      result[key] = transformResponseValue(res[key]);
    });
    return result;
  }
  if (toString.call(res) === '[object Array]') {
    return res.map((item: any) => transformResponseValue(item));
  }
  return res;
};
export default transformResponseValue;
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
摸鱼大侠想挣钱16 分钟前
flex居中布局引起滚动溢出问题
前端·css
itslife17 分钟前
tsconfig 配置
前端
顺丰同城前端技术团队19 分钟前
都2024年了 国际化你都不会做 附使用&源码阅读
前端·javascript
outsider_友人A20 分钟前
手摸手带你封装Vue组件库(16)Carousel走马灯组件
前端·javascript·vue.js
我是若尘23 分钟前
React 组件渲染机制:Props 变化时会发生什么?
前端
bigyoung23 分钟前
过滤tree数据中某些数据
前端·javascript·vue.js
禄钴钮恩24 分钟前
列表分页转无限滚动,使用Vue-infinite-loading实现
前端
console.log 战略储备局局长25 分钟前
TS类型体操:实现axios的链式调用类型提示
前端
小酒星小杜26 分钟前
为了投入AI的怀抱,将Nextjs项目从Vercel迁移到了CF,结果是好的,过程是痛苦的
前端·aigc·next.js
代码or搬砖1 小时前
ECharts实现数据可视化
前端·信息可视化·echarts