一起来看看 Facebook 是如何封装:请求超时之后,发起请求重试

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

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

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

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

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

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

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

ts 复制代码
// 发起请求
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 放置在一个闭包函数里面, 直接上代码:

ts 复制代码
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 , 那么如果接口时间过长, 会存在的情况是啥? 无限重复请求

就像下面这样子:

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

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

比如:

ts 复制代码
const DEFAULT_RETRIES = [1000, 3000];

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

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

ts 复制代码
const DEFAULT_RETRIES = [0, 0];

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

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

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

ts 复制代码
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 那么, 请求会直接成功, 只会请求一次即可:

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

5.彩蛋

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

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

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

直接启动命令行即可:

bash 复制代码
dyson ./src 3000

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

源码链接

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

相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端