我的前端请求我做主

1,786 阅读4分钟

近期开发任务严峻,在疯狂写代码(少点bug好不好)的同时还得抽出时间跟后端对接,于是有下面对话

我: 这个页面一共包含name,id,time,address, data五个字段,你们谁提供接口啊! 小明: 我提供一个接口,给你返回id, name, time字段。 小红:我也提供一个接口,我返给你address字段。 小张:我也是,我给你data字段。 我:你们是在为难我胖虎嘛???就五个字段我得请求三个接口,你们不能聚合一下嘛! 三位异口同声:我们现在是微服务只能提供这么多,前端自己聚合吧 我:卒···

上述例子在开发中经常遇到,解决倒是很简单,但如何保证页面的完整性是一个问题。

🌰

我们需要请求三个接口,分别是接口a、接口b和接口c。请求过程中由于服务不稳定导致a成功了但b和c失败了。这种情况我们要么直接报错误信息,有点不友好;要么只使用a返回的数据,一般这种情况前端都会有默认值,页面自然而然展示出了默认信息,用户就会用错误的数据做出错误的决策,这是很严重的错误。为了解决上述问题,本文实现了一个容错机制,去尝试解决这个问题。

实现

重试方案

首先重试机制意味着我们需要多次请求失败接口,先解决这个问题,大概两张方案:

  • socket
  • 轮询 但是我们没有后端支持,socket直接干掉。那么只能通过轮询去尝试。先贴出轮询代码
export interface IParams {
  maxCount?: number; // 最大轮询次数
  intervalTime?: number; // 每次轮询增加时间长度
  maxInterval?: number; // 最大轮询时间长度
}
export interface IProcessPayload<T> {
  data: T;
  count: number;
  resolve?: (data: T) => void;
  reject?: (err: any) => void;
}
/**
 * 
 * error 失败
 * process 继续轮询
 * finish 结束轮询
 */
export type IProgressType = 'error' | 'process' | 'finish';
const defaultConfig = {
  maxCount: 120,
  intervalTime: 1000,
  maxInterval: 1600,
};
export class PollingFun {
  timeoutTimer: any;
  cancelWhile: any;
  constructor(private config: IParams = { maxCount: 120, intervalTime: 1000, maxInterval: 1600 }) {
    this.config = { ...defaultConfig, ...config };
  }
  cancel() {
    if (this.cancelWhile) {
      this.cancelWhile();
      this.cancelWhile = null;
    }
    if (this.timeoutTimer) {
      clearTimeout(this.timeoutTimer);
    }
  }
  pollingSingleTask = async <T>(onProgress: (data: IProcessPayload<T>) => IProgressType, ajaxFun: () => Promise<T>) => {
    const { maxCount, intervalTime, maxInterval } = this.config;
    let pollingCount = 0;
    let stopPolling = false;
    this.cancel();
    this.cancelWhile = () => (stopPolling = true);
    while (!stopPolling && pollingCount < maxCount) {
      // 刚开始密集,后续间隔加长,最长1s。
      let realIntervalTime = Math.floor(pollingCount / 10) * 200 + intervalTime; // eslint-disable-line
      realIntervalTime = Math.min(realIntervalTime, maxInterval);
      try {
        const resData = await ajaxFun();
        if (stopPolling) {
          return Promise.reject('cancel');
        }
        const progressRes = onProgress({ data: resData, count: pollingCount });
        switch (progressRes) {
          case 'finish':
            stopPolling = true;
            return Promise.resolve(resData);
          case 'error':
            stopPolling = true;
            return Promise.reject(resData);
          default:
            await new Promise(resolve => {
              this.timeoutTimer = setTimeout(resolve, realIntervalTime);
            });
            break;
        }
      } catch (error) {
        stopPolling = true;
        return Promise.reject(error);
      }
      pollingCount += 1;
    }
    if (pollingCount >= maxCount) {
      return Promise.reject('overMaxCount');
    }
  };
}

可以看到我们实现了一个轮询类,使用方式也很简单,只需要每次new一个实例,然后调用对应的方法即可。轮询方法需要两个参数,一个是轮询处理函数,其接受一个参数,会携带本次轮询的数据,我们只需要对数据做判断,然后返回相应的数据处理轮询。

const pollInstance = new PollingFun();
pollInstance.pollingSingleTask(process, ajaxFun);

重试机制

由上述背景我们可以知道,请求成功意味着所有请求都返回了结果,脑袋一转,想到了Promise.all,瞬间解决了一半的问题。我们只需要把每个请求函数包裹成轮询的方式,然后等着拿值就行,上手开干!

type AjaxFun<T> = [() => Promise<T>, (data: IProcessPayload<T>) => IProgressType, IParams];
const createPromise = <T>(ajaxFunArr: AjaxFun<T>[]) => {
  return ajaxFunArr.map(item => {
    const [ajaxFn, onProcess, options] = item;
    const pollInstance = new PollingFun(options);
    return new Promise((resolve, reject) => {
      pollInstance.cancel();
      pollInstance
        .pollingSingleTask(payload => onProcess({ ...payload, resolve, reject }), ajaxFn)
        .catch(err => reject(err));
    });
  });
};
export const ajaxCatch = async <T>(ajaxFunArr: AjaxFun<T>[]) => {
  const wrapAjaxFunArr = await createPromise(ajaxFunArr);
  return Promise.all([...wrapAjaxFunArr])
    .then(res => ({
      status: true,
      data: res,
    }))
    .catch(err => ({
      status: false,
      data: err,
    }));
};

可以看到,我们封装了一个ajax处理函数,这个函数需要一个数组类型参数,每个数组子值需要提供有三个值,分别是当前请求函数、控制轮询状态的函数以及轮询初始化的值。看着还不错 试试效果

const wake = async val => {
  console.log(val);
  return await val;
};
const onProcess = pay => {
  const { data, resolve, count } = pay;
  if (data === 'q2' && count === 3) {
    resolve(data);
    return 'finish';
  }
  if (data === 'q2') {
    return 'process';
  }
  return 'finish';
};
export const getData = async () => {
  const res = await ajaxCatch([
    [() => wake('q1'), onProcess, { maxCount: 5 }],
    [() => wake('q2'), onProcess, { maxCount: 5 }],
    [() => wake('q3'), onProcess, { maxCount: 5 }],
  ]);
  console.log(res, 'cdc');
};

完美达到我们需要的效果!

总结

上述实现可以多次重试失败接口,并统一返回结果,用户也不会看到页面闪烁等问题,同时还可以当做轮询函数来使用,可谓一举两得!所以嘛,遇到问题不要慌,慢慢分析一步步解决!