前端应该如何封装高扩展的axios请求库

发布时间 2023-11-17 16:12:38作者: 柯基与佩奇

我看了很多 axios 的封装,但是我感觉他们的封装。也不够自由,主要是写完之后,如果以后有东西需要修改的时候,还要回去拦截器进行修改。但是有一些东西拦截器可能是你以后的业务需求才需要添加的。

我就在想我能不能拦截器做成插件式的模式进行动态配置呢?例如下面的效果,点击添加一个请求缓存器,请求的时候就读缓存。当然你也可以去掉直接到达效果。

实现这个功能我这需要下面的做法,就可以实现动态控制我们的 axios 来扩展请求业务。

this.interceptors.push(new xxxInterceptors());

动画.gif

如何进行考虑设计

首先为了让我们的系统拥有扩展性,我这里会先定义一个 IRequest interface,为以后要做的事情进行约束。我们平时使用 request 用的比较多就是只有 4 大请求方法。定义好接口之后,就是我们的实现方式了。

截屏2022-01-27 下午11.44.21.png

这样我们就可以把我们 node.js、fetch、小程序的 request 都实现然后给到系统调用层调用,他们不需要知道怎么实现,他们只需要知道我用什么类型的 Request 就 new 那个对象来进行使用即可。

在这个过程中,我们需要对 axios 进行重复请求处理。这个时候我需要一个 cache 来进行缓存。这一类的文章多得是,我和他们的做法不一样在于,我是使用 ES6 的 Map 做的 key,value 存储。我坚持使用 oop 去实现。React 除了视图使用 FC 模式去编写,处理业务的地方我还是推荐使用 oop 模式编码。

class AxiosHttpClient {
    private okHttp: AxiosInstance;
    private CancelToken = axios.CancelToken;
    private EXPIRE_TIME = 60000;

    private cache: Map<string | undefined, any> = new Map();

    public constructor() {
        this.okHttp = axios.create({ timeout: 10000, responseType: 'json' })

        this.okHttp.interceptors.request.use(config => {
            config = this.handleRequestCacheDoCacen(config);
            return config;
        })

        this.okHttp.interceptors.response.use(
            response => {
                response = this.handleResonseCacheDoCacen(response);
                return response;
            },
            err => {
                //已经取消的请求,我们需要把message 调到 response resolve里面进行正常的调用
                if (axios.isCancel(err)) {
                    return Promise.resolve(err.message);
                }
                return Promise.reject(err);
            }
        )
    }

    /**
     * 是否存在cache,如果存在取消请求
     */
    public handleRequestCacheDoCacen(config: AxiosRequestConfig<any>) {
        let source = this.CancelToken.source();
        config.cancelToken = source.token;
        let data = this.cache.get(config.url);
        if (data && GetTimeNowUnxi() - data.expire < this.EXPIRE_TIME) {
            source.cancel(data);
        }
        return config;
    }

     /**
     * 从缓存中获取Cache Data
     * @param response
     * @returns
     */
    public handleResonseCacheDoCacen(response: AxiosResponse<any>) {
        let data = {
            expire: GetTimeNowUnxi(),
            data: response.data
        };
        this.cache.set(`${response.config.url}`, data)
        return response;
    }

    public getHttpClient() {
        return this.okHttp;
    }

}

但是我写完后,发现interceptors就无法继续扩展了。如果后续需要维护我还得可能还要来这里进行修改。或者写以下 if 开关来处理逻辑。

这个时候你有很多种做法,用一个 Object/Array 来管理你的要做事情的handle,在interceptors里面进行for in / for of操作,这种做法类似与 pipe 流水线操作。不过我是偏向使用 rxjs 来做这种事情,但是前端按着发展速度,我不知道什么时候才有人意识到 rxjs。了解了 rxjs 后你会发现你那些vuex,dva,mobx等都是啥玩意。

为了照顾大多数前端,我这边设计使用的是观察者模式来实现interceptors热插槽处理。于是就有以下操作。

// index.js
class AxiosHttpClient {
    private okHttp: AxiosInstance;
    private CancelToken = axios.CancelToken;
    private $interceptorSubject: InterceptorSubject;

    private cache: Map<string | undefined, any> = new Map();
    private interceptors: Observer[] = []

    private static instance: AxiosHttpClient;
    public static getInstance() {
        if (!this.instance) {
            this.instance = new AxiosHttpClient();
        }
        return this.instance;
    }

    public constructor() {
        this.$interceptorSubject = new AxiosInterceptorSubject();
        this.okHttp = axios.create({ timeout: 10000, responseType: 'json' })
    }

    public getHttpClient() {

        this.okHttp.interceptors.request.use(config => {
            if (this.interceptors.length > 0) {
                for (let interceptor of this.interceptors) {
                    this.$interceptorSubject.attach(interceptor);
                }
                config = this.$interceptorSubject.notifyRequest(config);
            }

            return config;
        })

        this.okHttp.interceptors.response.use(
            response => {
                console.log(this.interceptors);
                if (this.interceptors.length > 0) {
                    response = this.$interceptorSubject.notifyResponse(response);
                }
                return response;
            },
            err => {
                //已经取消的请求,我们需要把message 调到 response resolve里面进行正常的调用
                if (axios.isCancel(err)) {
                    return Promise.resolve(err.message);
                }
                return Promise.reject(err);
            }
        )

        return this.okHttp;
    }

    public setCache(key: string, value: any) {
        this.cache.set(key, value);
    }

    public getCache(key: string | undefined) {
        return this.cache.get(key);
    }

    public getCancelToken() {
        return this.CancelToken;
    }

    public setInterceptorsObserver(ob: Observer) {
        this.interceptors.push(ob);
    }

    public removeInterceptorSubject(ob: Observer) {
        this.$interceptorSubject.detach(ob);
        const observerIndex = this.interceptors.indexOf(ob);
        this.interceptors.splice(observerIndex, 1);
    }

}

接口的定义

export interface InterceptorSubject {
  attach(observer: Observer): void;

  detach(observer: Observer): void;

  notifyRequest(config: AxiosRequestConfig): any;

  notifyResponse(response: AxiosResponse): any;
}

export interface Observer {
  update(config: AxiosRequestConfig): any;
  doResponse(config: AxiosResponse): any;
}

具体的实现类

import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { AxiosHttpClient, EXPIRE_TIME } from 'core/request/unitys/axios';
import { GetTimeNowUnxi } from 'core/time';
import { Observer } from './InterceptorSubject';


export class AxiosCacheInterceptor implements Observer {
    private app = AxiosHttpClient.getInstance();

    update(config: AxiosRequestConfig): any {
        let source = this.app.getCancelToken().source();
        config.cancelToken = source.token;
        let data = this.app.getCache(config.url);
        if (data && GetTimeNowUnxi() - data.expire < EXPIRE_TIME) {
            source.cancel(data);
        }
        return config;
    }

    doResponse(response: AxiosResponse<any, any>) {
        let data = {
            expire: GetTimeNowUnxi(),
            data: response.data
        };
        this.app.setCache(`${response.config.url}`, data)
        return response;
    }
}

然后再 App 的 main.ts 入口文件,把我需要的业务代码都注入到Subject里面进行调用监听。

image.png

我们现在就可以通过业务来动态控制我们的 axios 了...