前端接口请求截流,再也不用为重复请求发愁啦!

2,311 阅读3分钟

背景

在业务抽象的时候,我们通常会把与当前业务相关的所有内容都抽象到一个组件内。比如选人选部门这个功能。我们在抽象的时候,会把获取用户列表和部门列表数据的功能封装到选人选部门组件内部。

抽象后的问题

当该业务组件在一个页面被多次使用的时候,会产生多个实体,会导致组件内的接口被重复调用,性能下降。比如上述的选人选部门组件。 当一个页面有多个选人选部门组件的时候,每个组件都会发一次请求用户列表和部门列表的接口。 有同学可能会问了,为啥不给组件直接传数据源?这是因为很多地方用了这个组件,这样的处理别人用的时候,就不用关心它内部的数据源了,也可以把它发布到npm上!是不是很机智,哈哈哈哈!

解决办法

** 当前页面的所有相同组件的实例请求一次接口,相比较于发起请求后服务器端返回304,节约了资源,实现了客户端304**

难点

所有实例几乎是同时被渲然,同时执行组件的生命周期实体渲然是无序的,如何区分、记录每个实例对应的请求

解决方法

思路:内存缓存 + 异步

  1. 定义两个内存堆栈变量,用 url + params 作为key
  2. 第一个内存变量为 请求中的数据 pending
  3. 第二个内存变量为 请求返回的数据 resultData
  4. 对组件的请求request和拦截
  5. 每个请求都是一个Promise对象,pending堆 记录 resolve
  6. pending堆没有key时,发request请求
  7. pending堆能找到key 时,创建Promise 记录 resolve
  8. request回来的时候,从pending堆中找到相同key的 resolve方法,并且依次根据key 执行resolve
  9. resultData堆中根据key 记录返回的结果
  10. 在第4步中的拦截开始,根据key查找 resultData堆,如果有数据直接返回,没有数据从5开始。

具体实现



import axios, { AxiosRequestConfig } from "axios";

interface RequestThrottlerConfig {
  cacheMaxAgeMS: number;
}

interface CachedRequestRecord {
  data: any;
  expired: number;
}

export class RequestThrottler {
  conf: RequestThrottlerConfig;
  protected _map: {
    [key: string]: CachedRequestRecord;
  } = {};
  protected _pendingURLs: {
    [key: string]: { resolve: Function; reject: Function }[];
  } = {};
  constructor(conf?: RequestThrottlerConfig) {
    this.conf = { cacheMaxAgeMS: 20000, ...(conf || {}) };
  }
  protected getValidCachedRequest = (key: string) => {
    const req = this._map[key];

    if (req) {
      if (req.expired >= Date.now()) {
        return req;
      } else {
        delete this._map[key];
        return null;
      }
    }
    return null;
  };
  protected setPendingURL = (
    key: string,
    resolve?: Function,
    reject?: Function
  ) => {
    let list = this._pendingURLs[key] || (this._pendingURLs[key] = []);
    resolve && list.push({ resolve, reject: reject! });
  };
  protected setCachedRequest = (key: string, data: any) => {
    this._map[key] = {
      data,
      expired: this.conf.cacheMaxAgeMS + Date.now(),
    };
  };
  request = async (requestConf: AxiosRequestConfig) => {
    const key =
      (requestConf.method || "GET").toUpperCase() +
      "||" +
      requestConf.url! +
      `${requestConf.data ? "_data_" + JSON.stringify(requestConf.data) : ""}` +
      `${requestConf.params ? "_params" + JSON.stringify(requestConf.params) : ""
      }`;
    const validReq = this.getValidCachedRequest(key);
    if (validReq) {
      console.log("request", "cached", requestConf.url);

      return validReq;
    } else if (this._pendingURLs[key]) {
      console.log("request", "pending", requestConf.url);

      return new Promise((resolve, reject) => {
        this._pendingURLs[key].push({ resolve, reject });
      });
    } else {
      console.log("request", "fetch", requestConf.url);
      this.setPendingURL(key);
      return new Promise((resolve, reject) => {
        this._pendingURLs[key].push({ resolve, reject });
        axios(requestConf)
          .then((data) => {
            const list = this._pendingURLs[key];
            list.forEach((item) => {
              item.resolve(data);
            });
            delete this._pendingURLs[key];
            this.setCachedRequest(key, data);
          })
          .catch((ex) => {
            const list = this._pendingURLs[key];
            list.forEach((item) => {
              item.reject(ex);
            });
            delete this._pendingURLs[key];
          });
      });
    }
  };

}

const _requestThrottler = new RequestThrottler();

/**
 * 通用请求接口
 * @param requestConf 请求配置
 * @param throttle 是否限流
 */
export const requestThrottler = async (
  requestConf: AxiosRequestConfig,
  throttle?: boolean
) => {
  let data: any;
  if (throttle) {
    data = await _requestThrottler.request(requestConf);
  } else {
    data = await axios(requestConf);
  }
  return data;
};

相关知识

如果请求头中带有If-None-Match或If-Modified-Since,则会到源服务器进行有效性校验,如果源服务器资源没有变化,则会返回304;如果有变化,则返回200;