axios二次封裝

面向對象

雖然對面向對象理解不夠深,仍是嘗試使用面向對象的方法進行封裝。因此,選擇使用TypeScript做爲編程語言。主要的類關係以下圖:
image.png
AxiosSugar就是最主要的類,它依賴外部傳入axios(或axios的實例),AxiosSugarConfig(配置),AxiosSugarLifeCycle(生命週期/回調函數),AxiosSugarStorage(存儲類)。
因爲主要使用攔截器實現功能,因此該類只是一個「外部裝載」的類,它並不暴露方法,僅僅用於添加攔截器和保存它依賴的類的實例。ios

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,而後其它的類去實現它。npm

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,就中斷執行過程。axios

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. 判斷請求已經存儲this

    1. 若是是,中斷請求執行,直接跳到響應攔截器等待處理。
    2. 若是否,執行下一步

reject:spa

  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:code

  1. 將該請求移除棧
  2. 判斷是否存儲該響應結果orm

    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

相關文章
相關標籤/搜索