Axios二次封装,集成缓存

前言

最近写页面的时候把页面切成了多个组件,但是每个组件都需要一份字典的数据,如果用props传入的话就太过麻烦麻烦,想来想去还是对请求做一个缓存。

缓存的实现

我们首先要实现的是发起请求,如果响应成功后,接下来响应的接口都直接返回响应的数据。 这里我们可以用一个对象或者map将数据存起来,key的话可以用url加上请求数据生成。可以在axios请求里获取到这块数据。

js 复制代码
export class CacheManager {
  private cacheMap: Map<string, any> = new Map();

  /**
   * 根据请求生成key
   * @param config axios请求
   */
  public getCacheKey(config: AxiosRequestConfig): string {
    if (config.params) {
      return [config.method, config.url, ...Object.keys(config.params), ...Object.values(config.data)].join('&');
    } else if (config.data) {
      return [config.method, config.url, ...Object.keys(config.data), ...Object.values(config.data)].join('&');
    }
    return [config.method, config.url].join('&');
  }

  /**
   * 获取缓存
   * @param config axios请求
   */
  public get<T = any>(config: AxiosRequestConfig) {
    const key = this.getCacheKey(config);
    const cache = this.cacheMap.get(key);
    if (cache) {
      return cache;
    }
    return;
  }

  /**
   * 设置缓存
   * @param config axios请求
   * @param res 接口数据
   */
  public set(config: AxiosRequestConfig, res: any) {
    const key = this.getCacheKey(config);
    this.cacheMap.set(key, res)
  }
}

但是这样会会带来一个问题,如果我们在上一个接口还没响应的时候就发出第二个请求,这样我们的缓存就失去了左右,所以我们要给请求加上一个状态,完成和进行中。这样还会带来一个问题,如果把发起的请求暂停并且存储起来,等到响应后再发送过去呢?其实这个问题很好解决,接口的请求在前端无非是一个异步,一个异步只有两个状态,那我们只需要把这两个状态存起来就好,等到响应后根据接口放回对应的rosolve或reject。

所以我们这里可以这么改

js 复制代码
import type { AxiosRequestConfig } from 'axios';
import type { ResponseResult } from './http.interface';

export type CacheStatus = 'pending' | 'complete';
export type CacheCallBackType = 'success' | 'error';

export interface Cache {
  status: CacheStatus;
  res?: ResponseResult; // 这里是自己定义的后端返回格式,可以自己修改或者any
  callback: {
    resolve: (data: any) => void;
    reject: (error: any) => void;
  }[];
}

export class CacheManager {
  private cacheMap: Map<string, Cache> = new Map();

  /**
   * 根据请求生成key
   * @param config axios请求
   */
  public getCacheKey(config: AxiosRequestConfig): string {
    if (config.params) {
      return [config.method, config.url, ...Object.keys(config.params), ...Object.values(config.data)].join('&');
    } else if (config.data) {
      return [config.method, config.url, ...Object.keys(config.data), ...Object.values(config.data)].join('&');
    }
    return [config.method, config.url].join('&');
  }

  /**
   * 获取缓存
   * @param config axios请求
   */
  public get<T = any>(config: AxiosRequestConfig): Promise<ResponseResult<T>> | undefined {
    const key = this.getCacheKey(config);
    const cache = this.cacheMap.get(key);
    if (cache) {
      if (cache.status === 'complete') {
        return Promise.resolve(cache.res as ResponseResult<T>);
      }
      if (cache.status === 'pending') {
        return new Promise((resolve, reject) => {
          cache.callback.push({
            resolve,
            reject,
          });
        });
      }
    }
    this.cacheMap.set(key, {
      status: 'pending',
      callback: [],
    });
  }

  /**
   * 设置缓存
   * @param config axios请求
   * @param res 接口数据
   * @param cacheCallBackType Promise回调类型
   */
  public set(config: AxiosRequestConfig, res: any, cacheCallBackType: CacheCallBackType) {
    const key = this.getCacheKey(config);
    const cache = this.cacheMap.get(key);
    if (cache) {
      if (cacheCallBackType === 'success') {
        cache.res = res;
        cache.status = 'complete';
        for (let i = 0; i < cache.callback.length; i++) {
          cache.callback[i].resolve(res);
          cache.callback.splice(i, 1);
          i--;
        }
      } else if (cacheCallBackType === 'error') {
        for (let i = 0; i < cache.callback.length; i++) {
          cache.callback[i].reject(res);
          cache.callback.splice(i, 1);
          i--;
        }
        this.cacheMap.delete(key);
      }
    }
  }
}

封装Axios

封装Axios其实网上有很多文章了,我这里主要是借鉴了vben的封装,不是很难,就是把拦截器抽离出来而已。不同的是,我们都知道Axios会默认封装一层,接口响应的数据一般被放在了res.data里,但是我一般用不到,所以我把这一层直接去掉了。

直接贴上完整的代码

js 复制代码
import axios, {
  AxiosError,
  type AxiosInstance,
  type AxiosResponse,
  type InternalAxiosRequestConfig,
} from 'axios';
import { AxiosCanceler } from './AxiosCanceler';
import type { AxiosCreateOption, RequestOptions, ResponseResult } from './http.interface';
import { CacheManager } from './CacheManger';

/**
 * @description 封装axios 调用getInstance获取实例
 */
export class HttpRequest {
  private axiosInstance: AxiosInstance;
  private createOptions: AxiosCreateOption;
  private axiosCanceler: AxiosCanceler = new AxiosCanceler();
  private cacheManager: CacheManager = new CacheManager();

  constructor(createOptions: AxiosCreateOption) {
    this.axiosInstance = axios.create(createOptions);
    this.createOptions = createOptions;
    this.setInterceports();
  }

  /**
   * @description 设置拦截器和取消请求
   */
  private setInterceports(): void {
    this.axiosInstance.interceptors.request.use(
      (config: InternalAxiosRequestConfig & RequestOptions) => {
        // 判断是否开启重复请求
        let ignoreRepeatRequests = this.createOptions.ignoreRepeatRequests;
        ignoreRepeatRequests =
          config.ignoreRepeatRequests !== undefined
            ? config.ignoreRepeatRequests
            : ignoreRepeatRequests;
        ignoreRepeatRequests && this.axiosCanceler.addPending(config);

        if (this.createOptions.interceptors?.onRequest) {
          config = this.createOptions.interceptors.onRequest(config);
        }
        return config;
      },
      this.createOptions.interceptors?.onRequestError
    );

    this.axiosInstance.interceptors.response.use((res: AxiosResponse<ResponseResult>) => {
      this.axiosCanceler.removePending(res.config);
      if (this.createOptions.interceptors?.onResponse) {
        res = this.createOptions.interceptors.onResponse(res);
      }
      return res;
    }, this.createOptions.interceptors?.onResponseError);
  }

  /**
   * @description 移除所有请求
   */
  public allRequestCanceler(): void {
    this.axiosCanceler.removeAllPending();
  }

  /**
   * @param config axios请求数据
   */
  public request<T = any>(config: RequestOptions): Promise<ResponseResult<T>> {
    return new Promise(async (resolve, reject) => {
      // 判断是否开启缓存
      if (config.cache) {
        const cacheData = this.cacheManager.get<T>(config);
        // 不等于undefined意味着已经有正在pending状态了
        if (cacheData !== undefined) {
          try {
            return resolve(await cacheData);
          } catch (err) {
            return reject(err);
          }
        }
      }
      this.axiosInstance
        .request(config)
        .then((res: AxiosResponse<ResponseResult>) => {
          config.cache && this.cacheManager.set(config, res.data, 'success');
          resolve(res.data as ResponseResult<T>);
        })
        .catch((err: AxiosError<ResponseResult>) => {
          if (this.createOptions.interceptors?.onErrorCatch) {
            const newErr = this.createOptions.interceptors?.onErrorCatch(err);
            config.cache && this.cacheManager.set(config, newErr, 'error');
            return reject(newErr);
          }
          config.cache && this.cacheManager.set(config, err, 'error');
          reject(err);
        });
    });
  }
  public get<T = any>(
    url: string,
    config?: RequestOptions
  ): Promise<ResponseResult<T> | AxiosResponse<ResponseResult<T>>> {
    return this.request<T>({ ...config, url, method: 'get' });
  }
  public post<T = any>(
    url: string,
    data?: any,
    config?: RequestOptions
  ): Promise<ResponseResult<T> | AxiosResponse<ResponseResult<T>>> {
    return this.request<T>({ ...config, url, data, method: 'post' });
  }
  public put<T = any>(
    url: string,
    data?: any,
    config?: RequestOptions
  ): Promise<ResponseResult<T> | AxiosResponse<ResponseResult<T>>> {
    return this.request<T>({ ...config, url, data, method: 'put' });
  }
  public delete<T = any>(
    url: string,
    config?: RequestOptions
  ): Promise<ResponseResult<T> | AxiosResponse<ResponseResult<T>>> {
    return this.request<T>({ ...config, url, method: 'delete' });
  }
  public upload<T = any>(
    url: string,
    data?: any,
    config?: RequestOptions
  ): Promise<ResponseResult<T> | AxiosResponse<ResponseResult<T>>> {
    return this.request<T>({
      ...config,
      url,
      data,
      method: 'post',
      headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
    });
  }
}

这里是声明的类型

js 复制代码
import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

/**
 * @interface 创建实例参数
 */
export interface AxiosCreateOption extends AxiosRequestConfig {
  interceptors?: Interceptors;
  /**
   * @description 是否开启重复请求
   */
  ignoreRepeatRequests?: boolean;
}

/**
 * @interface 拦截器
 */
export interface Interceptors {
  /**
   * @description 请求拦截
   */
  onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;

  /**
   * @description 响应拦截
   */
  onResponse?: (res: AxiosResponse<ResponseResult>) => AxiosResponse<ResponseResult>;

  /**
   * @description: 请求拦截错误处理
   */
  onRequestError?: (error: AxiosError) => void;

  /**
   * @description: 响应拦截错误处理
   */
  onResponseError?: (error: AxiosError) => void;

  /**
   * @description 请求失败处理
   */
  onErrorCatch?: (err: AxiosError<ResponseResult>) => AxiosError | ResponseResult | undefined;
}

/**
 * @interface 请求数据参数
 */
export interface RequestOptions extends AxiosRequestConfig {
  /**
   * @description 是否开启缓存
   */
  cache?: boolean;
  /**
   * @description 是否开启重复请求
   */
  ignoreRepeatRequests?: boolean;
}

/**
 * @interface 响应数据格式
 */
export interface ResponseResult<T = any> {
  code: number;
  method: string;
  time: string;
  path: string;
  data: T;
  token?: string;
  message?: string;
}

接下来主要是介绍一下缓存功能的实现

这里我们能看到axiosCanceler和cacheManager,对应的是取消请求和缓存管理 axosCanceler跟vben差不多,直接贴代码

js 复制代码
import axios, { type AxiosRequestConfig, type Canceler } from 'axios';

export class AxiosCanceler {
  private pendingMap: Map<string, Canceler> = new Map<string, Canceler>();
  /**
   * 获取存放map的key值
   * @param config axios请求
   */
  private getCancelerKey(config: AxiosRequestConfig): string {
    if (config.params) {
      return [config.method, config.url, ...Object.keys(config.params), ...Object.values(config.data)].join('&');
    } else if (config.data) {
      return [config.method, config.url, ...Object.keys(config.data), ...Object.values(config.data)].join('&');
    }
    return [config.method, config.url].join('&');
  }
  /**
   * 增加请求
   * @param config axios请求
   */
  public addPending(config: AxiosRequestConfig): void {
    // 取消重复请求
    this.removePending(config);
    config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
      const key = this.getCancelerKey(config);
      if (!this.pendingMap.has(key)) {
        this.pendingMap.set(key, cancel);
      }
    });
  }
  /**
   * 移除请求
   * @param config axios请求
   */
  public removePending(config: AxiosRequestConfig): void {
    const key = this.getCancelerKey(config);
    const cancel = this.pendingMap.get(key);
    cancel && cancel(key + '!cancel');
    this.pendingMap.delete(key);
  }
  /**
   *  移除所有请求
   */
  public removeAllPending(): void {
    this.pendingMap.forEach((cancel: Canceler) => {
      cancel && cancel('cancel');
    });
    this.pendingMap.clear();
  }
}

使用

对接口进行单独设置

js 复制代码
import http from '../index';

export function getDictApi() {
  // 开启缓存
  return http.get('dict', { cache: true });
}

git

完整代码可以看这里 github.com/SLSSZM/axio...

相关推荐
黄尚圈圈32 分钟前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光5 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   5 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   5 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web5 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常5 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器