一起来看看 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...

相关推荐
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd4 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常4 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer4 小时前
Vite:为什么选 Vite
前端
小御姐@stella4 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing4 小时前
【React】增量传输与渲染
前端·javascript·面试
GISer_Jing4 小时前
WebGL在低配置电脑的应用
javascript
eHackyd4 小时前
前端知识汇总(持续更新)
前端