yanlele / node-index

学习笔记、博文、简书、工作日常踩坑记录以及一些独立作品的汇总目录

Home Page:https://yanlele.github.io/node-index

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

请求超时重试

yanlele opened this issue · comments

看过很多请求超时重试的样例, 很多都是基于 axios interceptors 实现的。 但是有没有牛逼的原生方式实现呢?

最近在看 fbjs 库里面的代码, 发现里面有一个超时重试的代码, 只有一百多行代码, 封装的极其牛逼。 直接贴代码地址:github.com/facebook/fb…

不过这里的代码是 Flow 类型检测的代码, 而且有一些外部小依赖, 接下来, 咱们解除依赖, 然后一步一步来实现一下这部分逻辑。

这里简单介绍一下 fbjs 这个库

fbjs(Facebook JavaScript)是一个由 Facebook 开发和维护的 JavaScript 工具库。它提供了一组通用的 JavaScript 功能和实用工具,用于辅助开发大型、高性能的 JavaScript 应用程序。

1.先封装一个正常的请求

我们先用 fetch 封装一个非常正常的请求, 这个没有什么好说的, 直接上代码:

// 发起请求
const sendTimedRequest = (url: string, fetchConfig: RequestInit) => {
  const request = fetch(url, fetchConfig);

  return new Promise((resolve, reject) => {
    request.then(response => {
      if (response.status >= 200 && response.status < 300) {
        resolve(response);
      } else {
        const error: any = new Error(`response error.`);
        error.response = response;
        reject(error);
      }
    }).catch(error => {
      reject(error);
    });
  });
};

2.请求超时判定

需要再次封装一个 参数 fetchTimeout, 这个参数的作用就是指明超时时间。 计算超时时间是从请求发起的时候开始计算, 如果超过 fetchTimeout 证明请求就超时了, 那么直接阻断该请求的;

要实现超时时间和阻断请求, 使用的原理也很简单, 就是 闭包 + setTimeout + flag

所以因为引入了闭包, 我们需要将上面的 sendTimedRequest 放置在一个闭包函数里面, 直接上代码:

interface InitWithRetries extends RequestInit {
  fetchTimeout?: number | null;
}

const DEFAULT_TIMEOUT = 1000 * 1.5;


const fetchWithRetries = (url: string, initWithRetries?: InitWithRetries): Promise<any> => {
  // fetchTimeout 请求超时时间
  // 请求
  const { fetchTimeout, ...init } = initWithRetries || {};

  // 超时时间
  const _fetchTimeout = fetchTimeout != null ? fetchTimeout : DEFAULT_TIMEOUT;

  // 开始时间
  let requestStartTime = 0;

  return new Promise((resolve, reject) => {
    // 申明发送请求方法
    const sendTimedRequest = (): void => {
      // 发起请求时间
      requestStartTime = Date.now();

      // 是否需要处理后续请求
      let isRequestAlive = true;

      // 发起请求
      const request = fetch(url, init);

      // 请求超时情况
      const requestTimeout = setTimeout(() => {
        // 需要阻断正常的请求返回
        isRequestAlive = false;

        // 需要重新发起请求
        sendTimedRequest();
      }, _fetchTimeout);

      // 正常请求发起
      request.then(response => {
        // 正常请求返回的场景, 清空定时器
        clearTimeout(requestTimeout);

        // 如果进入了超时流程, 那么正常返回的逻辑, 就直接阻断
        if (isRequestAlive) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response);
          } else {
            const error: any = new Error(`response error.`);
            error.response = response;
            reject(error);
          }
        }
      }).catch(error => {
        reject(error);
      });
    };

    sendTimedRequest();
  });
};

3.上面代码存在问题

上面的代码其实是存在问题的;我们设置的超时时间是 1.5s , 那么如果接口时间过长, 会存在的情况是啥? 无限重复请求

就像下面这样子:

image.png

那么接下来要解决的问题就是, 重复请求次数问题, 我们需要把重复发起请求的次数限定在一个可控范围内;那么就需要加入重复请求次数的概念。

重复请求次数的概念, fbjs 里面的设计就非常巧妙了。因为他是一个数组,每个元素都是数字,每个数字对应的就是延迟重复请求的时间。

比如:

const DEFAULT_RETRIES = [1000, 3000];

上面的设置中, 表示首次请求超时之后, 会再次发起两次重复请求, 第一次重复请求延迟时间为 1000 ms 的时候发起, 第二次重复请求延迟时间为 3000ms 的时候发起。如果两次重复请求均失败, 那么最后再把最终失败结果作为 promise.reject 返回。

再例如, 如果设置时间为:

const DEFAULT_RETRIES = [0, 0];

那么会重复请求 2 次, 不会进行延迟请求, 第一次请求如果超时时间为 1.5 秒之后, 接口没有返回, 那么会立马进行第一次重试请求, 第一次重试请求 1.5秒 之后, 接口还是没有返回, 就进行第二次重试请求。

同时还需要一个概念就是, 如何判定是否需要再次请求, 即 shouldRetry 函数, 判定需要是否发起重复请求;

说到这儿了, 直接上完整代码

interface InitWithRetries extends RequestInit {
  fetchTimeout?: number | null;
  retryDelays?: number[] | null;
}

const DEFAULT_TIMEOUT = 1000 * 1.5;
const DEFAULT_RETRIES = [0, 0];

const fetchWithRetries = (url: string, initWithRetries?: InitWithRetries): Promise<any> => {
  // fetchTimeout 请求超时时间
  // 请求
  const { fetchTimeout, retryDelays, ...init } = initWithRetries || {};

  // 超时时间
  const _fetchTimeout = fetchTimeout != null ? fetchTimeout : DEFAULT_TIMEOUT;

  // 重复时间数组
  const _retryDelays = retryDelays != null ? retryDelays : DEFAULT_RETRIES;

  // 开始时间
  let requestStartTime = 0;

  // 重试请求索引
  let requestsAttempted = 0;

  return new Promise((resolve, reject) => {
    // 申明发送请求方法
    const sendTimedRequest = (): void => {
      // 自增索引与请求次数
      requestsAttempted++;

      // 发起请求时间
      requestStartTime = Date.now();

      // 是否需要处理后续请求
      let isRequestAlive = true;

      // 发起请求
      const request = fetch(url, init);

      // 请求超时情况
      const requestTimeout = setTimeout(() => {
        // 需要阻断正常的请求返回
        isRequestAlive = false;

        // 需要重新发起请求
        if (shouldRetry(requestsAttempted)) {
          console.warn("fetchWithRetries: HTTP timeout, retrying.");
          retryRequest();
        } else {
          reject(new Error(
            `fetchWithRetries(): Failed to get response from server, tried ${requestsAttempted} times.`,
          ));
        }
      }, _fetchTimeout);

      // 正常请求发起
      request.then(response => {
        // 正常请求返回的场景, 清空定时器
        clearTimeout(requestTimeout);

        // 如果进入了超时流程, 那么正常返回的逻辑, 就直接阻断
        if (isRequestAlive) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response);
          } else if (shouldRetry(requestsAttempted)) {
            console.warn("fetchWithRetries: HTTP error, retrying.");
            retryRequest();
          } else {
            const error: any = new Error(`response error.`);
            error.response = response;
            reject(error);
          }
        }
      }).catch(error => {
        clearTimeout(requestTimeout);
        if (shouldRetry(requestsAttempted)) {
          retryRequest();
        } else {
          reject(error);
        }
      });
    };

    // 发起重复请求
    const retryRequest = (): void => {
      // 重复请求 delay 时间
      const retryDelay = _retryDelays[requestsAttempted - 1];

      // 重复请求开始时间
      const retryStartTime = requestStartTime + retryDelay;

      // 延迟时间
      const timeout = retryStartTime - Date.now() > 0 ? retryStartTime - Date.now() : 0;

      // 重复请求
      setTimeout(sendTimedRequest, timeout);
    };

    // 是否可以发起重复请求
    const shouldRetry = (attempt: number): boolean => attempt <= _retryDelays.length;

    sendTimedRequest();
  });
};

fetchWithRetries("http://127.0.0.1:3000/user/")

4.测试

测试代码就是上面的完整代码, 如果我们有一个接口, 1s 左右返回, 因为超时时间为 1.5 s 那么, 请求会直接成功, 只会请求一次即可:

image.png

那么, 如果接口时间改为 2 s 时间返回:

image.png

5.彩蛋

上面使用到了一个 mock 接口, 这里推荐一个非常非常非常好用的 mock 工具, 使用简单又好使: webpro/dyson

比如 mock 上面的 user 请求, 那么只需要下面代码就可以了: 文件 /src/index.js, 代码如下

module.exports = {
  path: '/user/',
  method: 'GET',
  delay: 2000,
  cache: false,
  template: (params, query, body, cookies, headers) => {
    return {
      message: 'success',
      status: 200,
    }
  }
}

直接启动命令行即可:

dyson ./src 3000

更多使用文档可以访问 github 官方文档

源码链接

直接丢链接: github.com/yanlele/nod…