Front-end api request caching scheme

In the development of web applications, performance is an indispensable topic. For web packaged single page applications, there are many ways to optimize performance, such as tree-shaking, module lazy loading, and using extrens network cdn to accelerate these conventional optimization. Even in the vue-cli project, we can use the -- modern instruction to generate new and old browser code to optimize the program.

In fact, caching must be one of the effective ways to improve web applications, especially when users are limited by network speed. Improve the responsiveness of the system and reduce network consumption. Of course, the closer the content is to the user, the faster the cache will be and the more effective the cache will be.

On the client side, we have many ways to cache data and resources, such as standard browser caching and the current hot Service worker. However, they are more suitable for caching static content. For example, html, js, css and pictures. And caching system data, I use another solution.

Now I will introduce the various api request schemes that I applied to the project, from simple to complex.

Scheme 1 Data Caching

Simple data caching, the first request to obtain data, then use data, no longer request back-end api.
The code is as follows:

const dataCache = new Map()

async getWares() {
    let key = 'wares'
    // Getting data from the data cache
    let data = dataCache.get(key)
    if (!data) {
        // No Data Request Server
        const res = await request.get('/getWares')
        
        // Other operations
        ...
        data = ...

        // Setting up data cache
        dataCache.set(key, data)

    }
    return data
} 

The first line of code uses maps with es6 or more. If you don't understand maps well, you can refer to them.
ECMAScript 6 Introduction Set and Map perhaps Exploring ES6 The introduction of map and set can be understood here as a key-value pair storage structure.

Then the code uses the async function, which makes the asynchronous operation more convenient. You can refer to it. ECMAScript 6 Initial async Function To learn or consolidate knowledge.

The code itself is easy to understand. It uses Map objects to cache data, and then calls to fetch data from Map objects. For its simple business scenario, you can use this code directly.

Call method:

getWares().then( ... )
// The second call takes the previous data
getWares().then( ... )

Scheme 2 promise cache

The first scheme is not enough in itself. Because if more than two calls to this api are considered at the same time, the second request api will be made because the request is not returned. Of course, if you add a single data source framework like vuex and redux to the system, this problem is not likely to be encountered, but sometimes we want to call APIs for each complex component separately, instead of communicating data between components.

const promiseCache = new Map()

getWares() {
    const key = 'wares'
    let promise = promiseCache.get(key);
    // There is no promise in the current promise cache
    if (!promise) {
        promise = request.get('/getWares').then(res => {
            // Operating on res
            ...
        }).catch(error => {
            // After the request comes back, if there is a problem, delete promise from cache to avoid the second request continuing to make an error S
            promiseCache.delete(key)
            return Promise.reject(error)
        })
    }
    // Return promise
    return promise
}

This code avoids the problem of multiple requests at the same time in scheme one. At the same time, promises are deleted in the case of back-end errors, so that there will be no problem that promises with caching errors will always go wrong.

Calling mode:

getWares().then( ... )
// Second call to get previous promise
getWares().then( ... )

Scheme 3 Multi-promise Cache

This scheme is to return data simultaneously when more than one api request is needed, if an api error occurs. No correct data is returned.

const querys ={
    wares: 'getWares',
    skus: 'getSku'
}
const promiseCache = new Map()

async queryAll(queryApiName) {
    // Determine whether the incoming data is an array?
    const queryIsArray = Array.isArray(queryApiName)
    // Unified processing of data, whether strings or arrays are treated as arrays
    const apis = queryIsArray ? queryApiName : [queryApiName]
    
    // Get all request services
    const promiseApi = []

    apis.forEach(api => {
        // Using promise 
        let promise = promiseCache.get(api)

        if (promise) {
            // If there is one in the cache, push directly
            promise.push(promise)
        } else {
             promise = request.get(querys[api]).then(res => {
                // Operating on res
                ...
                }).catch(error => {
                // After the request comes back, if there is a problem, delete promise from cache
                promiseCache.delete(api)
                return Promise.reject(error)
            })
            promiseCache.set(api, promise)
            promiseCache.push(promise)
        }
    })
    return Promise.all(promiseApi).then(res => {
        // Returns data based on whether a string or an array is passed in, because it is an array operation itself.
        // If you pass in a string, you need to take it out
        return queryIsArray ? res : res[0]
    })
}

This scheme is a way to obtain data from multiple servers at the same time. Multiple data can be obtained at the same time to operate without errors due to a single data problem.

Calling mode

queryAll('wares').then( ... )
// The second call will not fetch wares, but skus.
queryAll(['wares', 'skus']).then( ... )

Scheme 4 adds time-related caching

Often caching is harmful. If we know that the data has been modified, we can delete the cache directly. At this time, we can call the method to request from the server. In this way, we avoid displaying old data on the front end. But we may not operate on the data for a period of time, then the old data will always exist at this time, so we had better set a time to remove the data.
The scheme uses class persistent data as data cache, and adds expired data and parameterization.
The code is as follows:
First, define a persistence class that can store promise or data

class ItemCache() {
    construct(data, timeout) {
        this.data = data
        // Set timeout time, how many seconds
        this.timeout = timeout
        // The time when an object is created, approximately set to the time when the data is acquired
        this.cacheTime = (new Date()).getTime
    }
}

Then we define the data cache. We use the same api as Map

class ExpriesCache {
    // Define a static data map as a cache pool
    static cacheMap =  new Map()

    // Is the data timeout
    static isOverTime(name) {
        const data = ExpriesCache.cacheMap.get(name)
        
        // No data must be timed out
        if (!data) return true

        // Get the current timestamp of the system
        const currentTime = (new Date()).getTime()        
        
        // Get the past seconds of the current time and storage time
        const overTime = (currentTime - data.cacheTime) / 1000
        
        // If the number of seconds in the past is greater than the current timeout time, return null and let it go to the server to fetch data.
        if (Math.abs(overTime) > data.timeout) {
            // This code may or may not be a problem, but if there is this code, re-entering the method can reduce judgment.
            ExpriesCache.cacheMap.delete(name)
            return true
        }

        // No timeout
        return false
    }

    // Is the current data timeout in cache
    static has(name) {
        return !ExpriesCache.isOverTime(name)
    }

    // Delete data from cache
    static delete(name) {
        return ExpriesCache.cacheMap.delete(name) 
    }

    // Obtain
    static get(name) {
        const isDataOverTiem = ExpriesCache.isOverTime(name)
        //If the data timeout, return null, but not ItemCache object
        return isDataOverTiem ? null : ExpriesCache.cacheMap.get(name).data
    }

    // Default storage for 20 minutes
    static set(name, data, timeout = 1200) {
        // Setting itemCache
        const itemCache = mew ItemCache(data, timeout)
        //cache
        ExpriesCache.cacheMap.set(name, itemCache)
    }
}

At this point, the data class and the operation class have been defined, which we can define at the api level.

// Generating key value error
const generateKeyError = new Error("Can't generate key from name and argument")

// Generate key value
function generateKey(name, argument) {
    // Get the data from arguments and turn it into an array
    const params = Array.from(argument).join(',')
    
    try{
        // Returns a string, function name + function parameter
        return `${name}:${params}`
    }catch(_) {
        // Returns the generated key error
        return generateKeyError
    }
}

async getWare(params1, params2) {
    // Generate key
    const key = generateKey('getWare', [params1, params2]) 
    // Get data
    let data = ExpriesCache.get(key)
    if (!data) {
        const res = await request('/getWares', {params1, params2})
        // Using a 10s cache, after 10s, get again to get null and continue requesting from the server
        ExpriesCache.set(key, res, 10)
    }
    return data
}

The scheme uses different caching methods with different expiration time and api parameters. Most business scenarios can already be met.

Calling mode

getWares(1,2).then( ... )
// Second call to get previous promise
getWares(1,2).then( ... )
// Different parameters, no previous promise
getWares(1,3).then( ... )

Scheme 5: Modifier-based scheme 4

It is consistent with the solution of scheme 4, but based on modifier.
The code is as follows:

// Generating key value error
const generateKeyError = new Error("Can't generate key from name and argument")

// Generate key value
function generateKey(name, argument) {
    // Get the data from arguments and turn it into an array
    const params = Array.from(argument).join(',')
    try{
        // Return string
        return `${name}:${params}`
    }catch(_) {
        return generateKeyError
    }
}

function decorate(handleDescription, entryArgs) {
    // Determine whether the current final data is descriptor, and if it is descriptor, use it directly?
    // Modifiers such as log
    if (isDescriptor(entryArgs[entryArgs.length - 1])) {
        return handleDescription(...entryArgs, [])
    } else {
        // If not
        // Modifiers such as add(1) plus(20)
        return function() {
            return handleDescription(...Array.protptype.slice.call(arguments), entryArgs)
        }
    }
}

function handleApiCache(target, name, descriptor, ...config) {
    // Get the body of the function and save it
    const fn = descriptor.value
    // Modify function body
    descriptor.value = function () { 
        const key =  generateKey(name, arguments)
        // key cannot be generated, directly requesting server-side data
        if (key === generateKeyError)  {
            // Make requests using the function body you just saved
            return fn.apply(null, arguments)
        }
        let promise = ExpriesCache.get(key)
        if (!promise) {
            // Setting promise
            promise = fn.apply(null, arguments).catch(error => {
                 // After the request comes back, if there is a problem, delete promise from cache
                ExpriesCache.delete(key)
                // Return error
                return Promise.reject(error)
            })
            // Using a 10s cache, after 10s, get again to get null and continue requesting from the server
            ExpriesCache.set(key, promise, config[0])
        }
        return promise 
    }
    return descriptor;
}

// Develop modifiers
function ApiCache(...args) {
    return decorate(handleApiCache, args)
}

At this point, we use classes to cache the api

class Api {
    // Cache 10s
    @ApiCache(10)
    // Do not use the default value at this time, because the current modifier cannot get it.
    getWare(params1, params2) {
        return request.get('/getWares')
    }
}

There is no way to use functions as modifiers because functions have function lifts.
For example:

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

The code intends to counter equals 1 after execution, but the actual result is counter equals 0. Because of the function elevation, the code actually executed is as follows

@add
function foo() {
}

var counter;
var add;

counter = 0;

add = function () {
  counter++;
};

So there's no way to use modifiers on functions. Specific reference ECMAScript 6 Introduction Decorator
This approach is simple to write and has little impact on the business layer. But caching time cannot be dynamically modified

Call Method

getWares(1,2).then( ... )
// Second call to get previous promise
getWares(1,2).then( ... )
// Different parameters, no previous promise
getWares(1,3).then( ... )

summary

The caching mechanism and scenarios of api are also basically described here. It can basically complete the vast majority of data business caching. Here I also want to ask you if there is any better solution, or what is wrong in this blog. Welcome to correct, thank you here.
At the same time, there are a lot of unfinished work here, which may continue to improve in the blog in the future.

Keywords: Javascript network ECMAScript Vue

Added by sstoveld on Wed, 15 May 2019 11:10:25 +0300