面向对象

虽然对面向对象理解不够深,还是尝试使用面向对象的方法进行封装。所以,选择使用TypeScript作为编程语言。主要的类关系如下图:
image.png
AxiosSugar就是最主要的类,它依赖外部传入axios(或axios的实例),AxiosSugarConfig(配置),AxiosSugarLifeCycle(生命周期/回调函数),AxiosSugarStorage(存储类)。
由于主要使用拦截器实现功能,所以该类只是一个“外部装载”的类,它并不暴露方法,仅仅用于添加拦截器和保存它依赖的类的实例。

interface AxiosSugarOptions {
  config?: AxiosSugarConfig;
  storage?: AxiosSugarStorage;
  lifecycle?: AxiosSugarLifeCycle;
}

export default class AxiosSugar {
  private stack: AxiosSugarRequestStack = new AxiosSugarRequestStack();
  axios: AxiosInstance | AxiosStatic;
  config: AxiosSugarConfig = new AxiosSugarConfig();
  storage: AxiosSugarStorage = new AxiosSugarInnerStorage();
  lifecycle: AxiosSugarLifeCycle = new AxiosSugarLifeCycle();
  constructor (axios: AxiosInstance | AxiosStatic, options: AxiosSugarOptions = {}) {
    this.axios = axios;
    ['config', 'storage', 'lifecycle'].forEach(option => {
      if (options[option]) {
        this[option] = options[option];
      }
    });
    this.init();
  }
  private init (): void {
    // 设置拦截器
    requestInterceptors(this, this.stack);
    responseInterceptors(this, this.stack);
  }
}

派生Storage类

为了支持多种存储方式,又不希望最原始的Storage类可以实例化,将它设为接口,然后它的子类去实现它。
image.png
AxiosSugarStorage是一个接口,它指定三个必须实现的方法set, get, contains,然后其它的类去实现它。

export interface AxiosSugarStorage {
  set (symbol: string, res: any): void;
  get (symbol: string): any;
  contains (symbol: string): boolean;
}

export class AxiosSugarInnerStorage implements AxiosSugarStorage {
  data: {[key: string]: any} = {};
  set (symbol: string, res: any) {
    this.data[symbol] = res;
  }
  get (symbol: string): any {
    return this.data[symbol] || null;
  }
  contains (symbol: string): boolean {
    return typeof this.data[symbol] !== 'undefined';
  }
}

export class AxiosSugarInnerReleaseStorage extends AxiosSugarInnerStorage {
  // save time
  duration: number = 5 * 60 * 1000; // 5 minutes
  // volume limit
  limit: number = 15 * 1024 * 1024; // 15MB
  constructor (duration: number, limit: number) {
    super();
    if (isDef(duration)) this.duration = duration;
    if (isDef(limit)) this.limit = limit;
  }
  set (symbol: string, res: any) {
    let data = this.data;
    for (const [key, item] of Object.entries(data)) {
      if (getDurationMS(new Date().getTime(), item.time) >= this.duration) {
        delete data[key];
      }
    }
    if (sizeof(res) + sizeof(data) > this.limit) {
      data = this.data = {};
    }
    data[symbol] = {
      data: res,
      time: new Date().getTime()
    };
  }
  get (symbol: string): any {
    const target = this.data[symbol];
    return target ? target.data : null;
  }
}

export class AxiosSugarLocalStorage implements AxiosSugarStorage {
  set (symbol: string, res: any) {
    try {
      localStorage.setItem(symbol, JSON.stringify(res))
    } catch (err) {
      console.error(`[axios-sugar]: ${err.message}`)
    }
  }
  get (symbol: string) {
    const data = localStorage.getItem(symbol)

    return data === null ? null : JSON.parse(data)
  }
  contains (symbol: string) {
    return this.get(symbol) !== null
  }
}

栈类Stack

为了实现取消重复请求,所以需要一个类来存储每一个请求。请求开始时将其入栈,请求完成后将该请求出栈。然后在请求开始时判断是否栈中有相同的请求,就取消这个请求的发送。

export default class AxiosSugarRequestStack {
  private confs: AxiosRequestConfig[] = [];
  push (conf: AxiosRequestConfig) {
    this.confs.push(conf);
  }
  contains (conf: AxiosRequestConfig): boolean {
    return this.confs.indexOf(conf) >= 0;
  }
  remove (conf: AxiosRequestConfig) {
    const confs = this.confs;
    return confs.splice(confs.indexOf(conf), 1);
  }
  forEach (cb) {
    this.confs.forEach((conf, confIdx, thisArg) => {
      cb.call(conf, conf, confIdx, thisArg);
    });
  }
}

LifeCycle

为了对一些特定时期进行控制,需要一些回调函数的存在,现在用一个类的实例去指定。state是一个是否中断执行过程的标志,如果是false,就中断执行过程。

interface AxiosSugarLifeCycleResult {
  state: boolean;
  message: string;
}

export default class AxiosSugarLifeCycle {
  beforeRequest (conf: AxiosRequestConfig): AxiosSugarLifeCycleResult {
    return {
      state: true,
      message: ''
    }
  }
  beforeResponse (res: AxiosResponse): AxiosSugarLifeCycleResult {
    return {
      state: true,
      message: ''
    }
  }
}

主流程

主流程就是设置拦截器的过程。拦截器因为需要调用AxiosSugar保存的各种类的实例,所以需要传入它自己和私有的堆栈类给拦截器调用。
首先,设置请求的拦截器。它的执行过程是:
resolve:

  1. 判断请求是否重复

    1. 如果是,中断请求执行,直接跳到响应拦截器等待处理。
    2. 如果否,执行下一步
  2. 判断请求已经存储

    1. 如果是,中断请求执行,直接跳到响应拦截器等待处理。
    2. 如果否,执行下一步

reject:

  1. 中断请求执行,直接跳到响应拦截器等待处理。
export default function (
  sugar: AxiosSugar,
  stack: AxiosSugarRequestStack
): void {
  const axios = sugar.axios;
  const storage = sugar.storage;
  const conf = sugar.config;
  const lifecycle = sugar.lifecycle;
  let error: AxiosSugarError | Error;

  axios.interceptors.request.use(config => {
    config = normalizeProp(config, conf.prop);

    if (stack.contains(config)) {
      error = { reason: 'existed' };
      return Promise.reject(error);
    }

    // get custom options
    const custom = config[conf.prop];
    let isSave = conf.isSave;
    if (custom) {
      isSave = notUndef(custom.isSave, isSave);
    }
    if (isSave) {
      const storageRes = storage.get(genSymbol(config));
      if (storageRes) {
        error = { reason: 'saved', data: storageRes };
        return Promise.reject(error);
      }
    }

    const cycleRes = lifecycle.beforeRequest(config);

    if (!cycleRes.state) {
      error = { reason: 'beforeRequestBreak', message: cycleRes.message };
      return Promise.reject(error);
    }

    // send request
    stack.push(config);
    return Promise.resolve(config);
  }, err => {
    Promise.reject(err);
  });
}

返回是响应拦截器。它的执行过程是:
resolve:

  1. 将该请求移除栈
  2. 判断是否存储该响应结果

    1. 如果是,存储,执行下一步
    2. 如果否,执行下一步

reject:

  1. 处理一些自定义的错误(请求拦截器产生)
  2. 将该请求移除栈
  3. 是否重传该请求

    1. 如果是,重新发送请求,执行下一步
    2. 如果否,执行下一步
export default function (
  sugar: AxiosSugar,
  stack: AxiosSugarRequestStack
): void {
  const axios = sugar.axios;
  const storage = sugar.storage;
  const conf = sugar.config;
  const lifecycle = sugar.lifecycle;
  let error: AxiosSugarError | Error;

  axios.interceptors.response.use(res => {
    const config = res.config;
    const resData = res.data;
    
    if (config) {
      stack.remove(config);
    }
    
    const cycleRes = lifecycle.beforeResponse(res);
    if (!cycleRes.state) {
      error = { reason: 'beforeResponseBreack', message: cycleRes.message };
      return Promise.reject(error);
    }

    // get custom option
    const custom = config.custom;
    let isSave;
    if (custom) {
      isSave = notUndef(custom.isSave, conf.isSave);
    }

    // generate symbol string
    if (isSave) {
      const symbol = genSymbol(config);
      storage.set(symbol, resData);
    }

    return Promise.resolve(resData);
  }, err => {
    // if AxiosSugarError
    const reason = err.reason;

    if (reason) {
      return reason === 'saved' ? Promise.resolve(err.data) : Promise.reject(err);
    }

    const config = err.config;
    // axios error
    if (config) {
      // remove this request in stack
      stack.remove(config);

      // get custom options
      const custom = config[conf.prop];
      let isResend = conf.isResend,
        resendDelay = conf.resendDelay,
        resendTimes = conf.resendTimes,
        curResendTimes = 0;
      if (custom) {
        isResend = notUndef(custom.isResend, isResend);
        resendDelay = notUndef(custom.resendDelay, resendDelay);
        resendTimes = notUndef(custom.resendTimes, resendTimes);
        curResendTimes = notUndef(custom.curResendTimes, 0);
      }
      if (isResend) {
        if (curResendTimes < resendTimes) {
          return new Promise(resolve => {
            setTimeout(() => {
              if (!custom) {
                config.custom = {};
              }
              config.custom.curResendTimes = ++curResendTimes;
              if (isStr(config.data)) {
                config.data = JSON.parse(config.data);
              }
              return resolve(axios.request(config));
            }, resendDelay);
          });
        } else {
          error = {reason: 'resendEnd', message: `Can't get a response.`};
          return Promise.reject(error);
        }
      } else {
        return Promise.reject(err);
      }
    }
  });
}

具体用法见axios-sugar


小结点
154 声望3 粉丝