Axios request cache and repeated request filtering package (plus)

Posted May 27, 20208 min read

Foreword

Website performance optimization is an indispensable process for every website, and the most direct and simple is the optimization of http requests, including reducing the number of http requests and accelerating the speed of http requests.
In this article, we will start with the number of http requests and filter out all useless and invalid requests.

Start

This article will encapsulate http requests based on the axios open source library, including two small optimizations of request cache and repeated request filtering

The first step

First create a http-helper.js file, which encapsulates the above related functions based on axios
First of all, the content is like this:

import axios from 'axios';

const http = axios.create();

export default http

The above is simply exporting an axios instance for the project to use

Add request cache function

Then, with the cache function, we must define the cache hit. The cache hit I defined refers to: http request URLs are the same, request parameters are the same, and request types are the same. If the above three are the same, then It is considered that the cache is allowed to hit. Finally, according to the "cache expiration time", it is judged whether to obtain the latest data or fetch it from the cache.
The following process is organized:

  1. Initiate the request, set whether the request is cached, and how long it is cached
  2. Axios request interception, determine whether the request is set to cache, yes? Then it is judged whether the cache hits and expires, no? Then continue to initiate the request
  3. Axios responds to interception and judges whether the request result is cached, yes? Then cache the data and set the key value and expiration time

For the above process, there are a few points to confirm:

  1. How to terminate the request when the cache hits
    In axios, a clearToken can be set for each request. When the request cancellation method is called, the request is terminated, and the terminated message is returned to the request method through reject.
  2. When the cache hits, and return the cached data to the request method through resolve(), instead of getting the cached data in reject

Then the specific code can be like this:

//http-helper.js
import axios from 'axios';

const http = axios.create();

http.interceptors.request.use((config) => {
   /**
     * Generate a cancleToken for every request
     * /
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;
   /**
     * Try to get cached data
     * /
    const data = storage.get(cryptoHelper.encrypt(
        config.url + JSON.stringify(config.data) +(config.method || ''),
   ));
   /**
    * Judge whether the cache hits and has not expired
    * /
    if(data &&(Date.now() <= data.exppries)) {
        console.log(`Interface:${config.url} cache hit-${Date.now()}-${data.exppries}`);
       /**
        * Pass the cached data back to the request method through the cancle method
        * /
        source.cancel(JSON.stringify({
            type:CANCELTTYPE.CACHE,
            data:data.data,
        }));
    }
    return config;
});

http.interceptors.response.use((res) => {
    if(res.data && res.data.type === 0) {
        if(res.config.data) {
           /**
            * Get request body parameters
            * /
            const dataParse = JSON.parse(res.config.data);
            if(dataParse.cache) {
                if(! dataParse.cacheTime) {
                    dataParse.cacheTime = 1000 * 60 * 3;
                }
               /**
                * Encryption
                * Cache
                * /
                storage.set(cryptoHelper.encrypt(res.config.url + res.config.data +(res.config.method || '')), {
                    data:res.data.data, //response body data
                    exppries:Date.now() + dataParse.cacheTime, //Set the expiration time
                });
                console.log(`Interface:${res.config.url} Set cache, cache time:${dataParse.cacheTime}`);
            }
        }
        return res.data.data;
    } else {
        return Promise.reject('Interface reported wrong!');
    }
});

/**
* Package get, post request
* Integrated interface cache expiration mechanism
* Expired cache will re-request the latest data and update the cache
* Data is stored in localstorage
* {
* cache:true
* cacheTime:1000 * 60 * 3-default cache for 3 minutes
*}
* /
const httpHelper = {
get(url, params) {
return new Promise((resolve, reject) => {
http.get(url, params) .then(async(res) => {
resolve(res);
}). catch((error) => {
if(axios.isCancel(error)) {
const cancle = JSON.parse(error.message);
if(cancle.type === CANCELTTYPE.REPEAT) {
return resolve([]);
} else {
return resolve(cancle.data);
}
} else {
return reject(error);
}
});
});
},
post(url:string, params:any) {
return new Promise((resolve, reject) => {
http.post(url, params) .then(async(res) => {
resolve(res);
}). catch((error:AxiosError) => {
if(axios.isCancel(error)) {
const cancle = JSON.parse(error.message);
if(cancle.type === CANCELTTYPE.REPEAT) {
return resolve(null);
} else {
return resolve(cancle.data);
}
} else {
return reject(error);
}
});
});
},
};
export default httpHelper

In the above code, some things are not explained:

  1. Among them, storage is a cached data class encapsulated by itself. You can have .get, .set and other methods. CryptoHelper is an encapsulated MD5 encryption library, which mainly encrypts request url, request data through MD5 The concatenated string of the request type, etc., to obtain the data in the cache through the encrypted key(because the concatenated string is too long, encrypted by MD5, it will be much shorter)
  2. Why do you need to encapsulate a httpHelper alone, because the information inaxios.CancelToken.source(). Cancle(***)can only be obtained in the reject, and for the purpose of cache hit, it can still be on To obtain the correct data, you need to deal with this situation separately.

Add duplicate request filtering function

Rule:Mainly based on the latest request, that is, the latest repeat request, will interrupt the previous repeat request
The approximate process is as follows:

  1. Make a request
  2. Axios request interception, judge whether there is the same request in the request list array, yes? All repeated requests before termination, no? Add the current request to the request array and eventually continue to request
  3. Axios response interceptor, delete the current request from the request array

The specific code is as follows:

//http-helper.js
import axios from 'axios';

const http = axios.create();

const pendingRequests = [];

http.interceptors.request.use((config) => {
   /**
     * Generate a cancleToken for every request
     * /
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;
    //.... Omit part of the code
   /**
     * Repeat request judgment
     * Same url, same request type as duplicate request
     * Subject to the latest request
     * /
    const md5Key = cryptoHelper.encrypt(config.url +(config.method || ''));
   /**
     * Cancel all previous repeated and unfinished requests
     * /
    const hits = pendingRequests.filter((item) => item.md5Key === md5Key);
    if(hits.length> 0) {
        hits.forEach((item) => item.source.cancel(JSON.stringify({
            type:CANCELTTYPE.REPEAT,
            data:'Repeat request to cancel',
        })));
    }
   /**
     * Add the current request to the request list
     * /
    pendingRequests.push({
        md5Key,
        source,
    });
    return config;
});

http.interceptors.response.use((res) => {
   /**
     * Regardless of whether the request is successful,
     * Remove the completed request from the request queue
     * /
    //Obtain the encrypted string in the same encryption mode(MD5)
    const md5Key = cryptoHelper.encrypt(res.config.url +(res.config.method || ''));
    const index = pendingRequests.findIndex((item) => item.md5Key === md5Key);
    if(index> -1) {
        pendingRequests.splice(index, 1);
    }
    //.... Omit part of the code
});

//.... Omit part of the code

In fact, the logic is very simple, you can maintain the request list through an array

Final results

Written in ts, need to use can be changed to js
Since both cache and termination of repeated requests need to use source.cancle, a type value is needed, which distinguishes between cache hit termination and repeated request termination. The code is the CANCELTTYPE constant.
http-helper.ts

import axios, {CancelTokenSource, AxiosResponse, AxiosRequestConfig, AxiosError} from 'axios';
import Storage from './storage-helper';
import CryptoHelper from './cryptoJs-helper';

const CANCELTTYPE = {
    CACHE:1,
    REPEAT:2,
};

interface ICancel {
    data:any;
    type:number;
}

interface Request {
    md5Key:string;
    source:CancelTokenSource;
}
const pendingRequests:Request []= [];

const http = axios.create();
const storage = new Storage();
const cryptoHelper = new CryptoHelper('cacheKey');

http.interceptors.request.use((config:AxiosRequestConfig) => {
   /**
     * Generate a cancleToken for every request
     * /
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;
   /**
     * Cache hit judgment
     * If successful, cancel the current request
     * /
    const data = storage.get(cryptoHelper.encrypt(
        config.url + JSON.stringify(config.data) +(config.method || ''),
   ));
    if(data &&(Date.now() <= data.exppries)) {
        console.log(`Interface:${config.url} cache hit-${Date.now()}-${data.exppries}`);
        source.cancel(JSON.stringify({
            type:CANCELTTYPE.CACHE,
            data:data.data,
        }));
    }
   /**
     * Repeat request judgment
     * Same url, same request type as duplicate request
     * Subject to the latest request
     * /
    const md5Key = cryptoHelper.encrypt(config.url +(config.method || ''));
   /**
     * Cancel all previous repeated and unfinished requests
     * /
    const hits = pendingRequests.filter((item) => item.md5Key === md5Key);
    if(hits.length> 0) {
        hits.forEach((item) => item.source.cancel(JSON.stringify({
            type:CANCELTTYPE.REPEAT,
            data:'Repeat request to cancel',
        })));
    }
   /**
     * Add the current request to the request list
     * /
    pendingRequests.push({
        md5Key,
        source,
    });
    return config;
});

http.interceptors.response.use((res:AxiosResponse) => {
   /**
     * Regardless of whether the request is successful,
     * Remove the completed request from the request queue
     * /
    //Obtain the encrypted string in the same encryption mode(MD5)
    const md5Key = cryptoHelper.encrypt(res.config.url +(res.config.method || ''));
    const index = pendingRequests.findIndex((item) => item.md5Key === md5Key);
    if(index> -1) {
        pendingRequests.splice(index, 1);
    }
    if(res.data && res.data.type === 0) {
        if(res.config.data) {
            const dataParse = JSON.parse(res.config.data);
            if(dataParse.cache) {
                if(! dataParse.cacheTime) {
                    dataParse.cacheTime = 1000 * 60 * 3;
                }
                storage.set(cryptoHelper.encrypt(res.config.url + res.config.data +(res.config.method || '')), {
                    data:res.data.data,
                    exppries:Date.now() + dataParse.cacheTime,
                });
                console.log(`Interface:${res.config.url} Set cache, cache time:${dataParse.cacheTime}`);
            }
        }
        return res.data.data;
    } else {
        return Promise.reject('Interface reported wrong!');
    }
});

/**
* Package get, post request
* Integrated interface cache expiration mechanism
* Expired cache will re-request the latest data and update the cache
* Data is stored in localstorage
* {
* cache:true
* cacheTime:1000 * 60 * 3-default cache for 3 minutes
*}
* /
const httpHelper = {
get(url:string, params:any) {
return new Promise((resolve, reject) => {
http.get(url, params) .then(async(res:AxiosResponse) => {
resolve(res);
}). catch((error:AxiosError) => {
if(axios.isCancel(error)) {
const cancle:ICancel = JSON.parse(error.message);
if(cancle.type === CANCELTTYPE.REPEAT) {
return resolve([]);
} else {
return resolve(cancle.data);
}
} else {
return reject(error);
}
});
});
},
post(url:string, params:any) {
return new Promise((resolve, reject) => {
http.post(url, params) .then(async(res:AxiosResponse) => {
resolve(res);
}). catch((error:AxiosError) => {
if(axios.isCancel(error)) {
const cancle:ICancel = JSON.parse(error.message);
if(cancle.type === CANCELTTYPE.REPEAT) {
return resolve(null);
} else {
return resolve(cancle.data);
}
} else {
return reject(error);
}
});
});
},
};

export default httpHelper;

cryptoJs-helper.ts

import cryptoJs from 'crypto-js';

class CryptoHelper {
    public key:string;
    constructor(key:string) {
       /**
        * If you need a secret key, you can pass it in during instantiation
        * /
        this.key = key;
    }
   /**
     * Encryption
     * @param word
     * /
    public encrypt(word:string | undefined):string {
        if(! word) {
            return '';
        }
        const encrypted = cryptoJs.MD5(word);
        return encrypted.toString();
    }
}

export default CryptoHelper;

storage-helper.ts

class Storage {
    public get(key:string | undefined) {
        if(! key) {return;}
        const text = localStorage.getItem(key);
        try {
            if(text) {
                return JSON.parse(text);
            } else {
                localStorage.removeItem(key);
                return null;
            }
        } catch {
            localStorage.removeItem(key);
            return null;
        }
    }
    public set(key:string | undefined, data:any) {
        if(! key) {
            return;
        }
        localStorage.setItem(key, JSON.stringify(data));
    }
    public remove(key:string | undefined) {
        if(! key) {
            return;
        }
        localStorage.removeItem(key);
    }
}

export default Storage;

If you have any questions, I hope to correct me, thanks!