前端魔法堂:手写缓存模块

前言

之前系统接入大数据PV统计平台,最近因PV统计平台侧服务器资源紧张,要求各接入方必须缓存API调用验证用的Token,从而减少无效请求和服务端缓存中间件的存储压力。
虽然系统部分业务模块都有缓存数据的需求,但由于没有提供统一的前端缓存模块,这导致各业务模块都自行实现一套刚好能用的缓存机制,甚至还会导致内存泄漏。
以兄弟部门这张整改工单作为契机,是时候开发一个系统级的前端缓存模块,逐步偿还技术负债了。

1分钟上手指南

  1. 直接使用CacheManager
// 提供3种级别的缓存提供器
// 1. 当前Browser Context级缓存MemoryCacheProvider
// 2. 基于SessionStorage的SessionCacheProvider
// 3. 基于LocalStorage的LocalCacheProvider
const cache = new CacheManager(MemoryCacheProvider.default())

console.log(cache.get('token') === CacheManager.MISSED) // 回显true,缓存击穿时返回CacheManager.MISSED
cache.set('token1', (Math.random()*1000000).toFixed(0), 5000) // 缓存同步求值表达式结果5000毫秒

// 缓存异步求值表达式求值成功,缓存结果5000毫秒
cache.set('token2', new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hi there!')
    }, 6000)
}, 5000)
cache.get('token2').then(console.log, console.error) // 6000毫秒后回显 'hi there!'
setTimeout(() => {
    // 回显true,对于最终状态为fulfilled的异步求值操作,缓存有效创建时间为从pending转换为fulfilled的那一刻。
    // 因此token2缓存的有效时长为6000+5000毫秒。
    console.log(cache.get('token2') === CacheManager.MISSED)
}, 12000)

// 缓存异步求值表达式求值失败,中止缓存操作
cache.set('token3', new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('hi there!')
    }, 6000)
}, 5000)
cache.get('token3').then(console.log, console.error) // 6000毫秒后回显 'hi there!'
setTimeout(() => {
    console.log(cache.get('token3') === CacheManager.MISSED) // 7000毫秒后回显true
}, 7000)
  1. 高阶函数——memorize
function getToken(deptId, sysId){
    console.log(deptId, sysId)
    return new Promise(resolve => setTimeout(() => resolve('Bye!'), 5000))
}

// 缓存函数返回值10000毫秒
// 第三个参数默认值为MemoryCacheProvider.default()
const getCachableToken = memorize(getToken, 10000, MemoryCacheProvider.default())
getCachableToken(1,1).then(console.log) // 立即回显 '1 1',5000毫秒后回显 'Bye!'
getCachableToken(1,1).then(console.log) // 不再调用getToken方法,因此没有立即回显 '1 1',5000毫秒后回显 'Bye!'
getCachableToken(1,2).then(console.log) // 立即回显 '1 2',5000毫秒后回显 'Bye!'

Coding

CacheItem.js

class CacheItem {                                                                                                                                                                                             
    constructor(timeout, value, status) {                                                                                                                                                                     
        this.timeout = timeout                                                                                                                                                                                
        this.value = value                                                                                                                                                                                    
        this.created = (+new Date())                                                                                                                                                                          
        this.status = status || CacheItem.STATUS.SYNC                                                                                                                                                         
    }                                                                                                                                                                                                         
    isExpired() {                                                                                                                                                                                             
        return (this.timeout + this.created < (+new Date())) && (this.status !== CacheItem.STATUS.PENDING)                                                                                                    
    }                                                                                                                                                                                                         
    isSync() {                                                                                                                                                                                                
        return this.status === CacheItem.STATUS.SYNC                                                                                                                                                          
    }                                                                                                                                                                                                         
    isPending() {                                                                                                                                                                                             
        return this.status === CacheItem.STATUS.PENDING                                                                                                                                                       
    }                                                                                                                                                                                                         
    isFulfilled() {                                                                                                                                                                                           
        return this.status === CacheItem.STATUS.FULFILLED                                                                                                                                                     
    }                                                                                                                                                                                                         
    isRejected() {                                                                                                                                                                                            
        return this.status === CacheItem.STATUS.REJECTED                                                                                                                                                      
    }                                                                                                                                                                                                         
    expire() {                                                                                                                                                                                                
        this.timeout = 0                                                                                                                                                                                      
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    pending() {                                                                                                                                                                                               
        this.status = CacheItem.STATUS.PENDING                                                                                                                                                                
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    fulfill(value) {                                                                                                                                                                                          
        this.value = value                                                                                                                                                                                    
        this.status = CacheItem.STATUS.FULFILLED                                                                                                                                                              
        this.created = (+new Date())                                                                                                                                                                          
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    reject(error) {                                                                                                                                                                                           
        this.value = error                                                                                                                                                                                    
        this.status = CacheItem.STATUS.REJECTED                                                                                                                                                               
        this.expire()                                                                                                                                                                                         
    }                                                                                                                                                                                                         
    toString() {                                                                                                                                                                                              
        return JSON.stringify(this)                                                                                                                                                                           
    }                                                                                                                                                                                                         
}
CacheItem.STATUS = {                                                                                                                                                                                          
    SYNC: 0,                                                                                                                                                                                                  
    PENDING: 1,                                                                                                                                                                                               
    FULFILLED: 2,                                                                                                                                                                                             
    REJECTED: 3                                                                                                                                                                                               
}                                                                                                                                                                                                             
CacheItem.of = (timeout, value, status) => {                                                                                                                                                                  
    if (typeof timeout === 'string' && value === undefined && status === undefined) {                                                                                                                         
        // Parse cache item serialized presentation to CacheItem instance.                                                                                                                                    
        const proto = JSON.parse(timeout)                                                                                                                                                                     
        const cacheItem = new CacheItem(proto.timeout, proto.value, proto.status)                                                                                                                             
        cacheItem.created = proto.created                                                                                                                                                                     
        return cacheItem                                                                                                                                                                                      
    }                                                                                                                                                                                                         
    else {                                                                                                                                                                                                    
        return new CacheItem(timeout, value, status)                                                                                                                                                          
    }                                                                                                                                                                                                         
}   

CacheManager.js

class CacheManager {                                                                                                                                                                                          
    constructor(cacheProvider, id) {                                                                                                                                                                          
        this.provider = cacheProvider                                                                                                                                                                         
        this.id = id                                                                                                                                                                                          
    }                                                                                                                                                                                                         
    key(name) {                                                                                                                                                                                               
        return (this.id != null ? this.id + '-' : '') + String(name)                                                                                                                                          
    }                                                                                                                                                                                                         
    set(name, value, timeout) {                                                                                                                                                                               
        const key = this.key(name)                                                                                                                                                                            
                                                                                                                                                                                                              
        if (value && value.then) {                                                                                                                                                                            
            // Cache thenable object                                                                                                                                                                          
            this.provider.set(key, CacheItem.of(timeout).pending())                                                                                                                                           
            value.then(value => this.provider.get(key).fulfill(value)                                                                                                                                         
                       , error => this.provider.get(key).reject(error))                                                                                                                                       
        }                                                                                                                                                                                                     
        else {                                                                                                                                                                                                
            this.provider.set(key, CacheItem.of(timeout, value))                                                                                                                                              
        }                                                                                                                                                                                                     
    }                                                                                                                                                                                                         
    get(name) {                                                                                                                                                                                               
        const key = this.key(name)                                                                                                                                                                            
        const cacheItem = this.provider.get(key)                                                                                                                                                              
        if (null === cacheItem) return CacheManager.MISSED                                                                                                                                                    
                                                                                                                                                                                                              
        if (cacheItem.isExpired()) {                                                                                                                                                                          
            this.provider.remove(key)                                                                                                                                                                         
            return CacheManager.MISSED                                                                                                                                                                        
        }                                                                                                                                                                                                     
        else if (cacheItem.isSync()) {                                                                                                                                                                        
            return cacheItem.value                                                                                                                                                                            
        }                                                                                                                                                                                                     
        else if (cacheItem.isFulfilled()) {                                                                                                                                                                   
            return Promise.resolve(cacheItem.value)                                                                                                                                                           
        }                                                                                                                                                                                                     
        else if (cacheItem.isPending()) {                                                                                                                                                                     
            return new Promise((resolve, reject) => {                                                                                                                                                         
                let hInterval = setInterval(() => {                                                                                                                                                           
                    let item = this.provider.get(key)                                                                                                                                                         
                    if (item.isFulfilled()) {                                                                                                                                                                 
                        clearInterval(hInterval)                                                                                                                                                              
                        resolve(item.value)                                                                                                                                                                   
                    }                                                                                                                                                                                         
                    else if (item.isRejected()) {                                                                                                                                                             
                        clearInterval(hInterval)                                                                                                                                                              
                        reject(item.value)                                                                                                                                                                    
                    }                                                                                                                                                                                         
                                                                                                                                                                                                              
                }, CacheManager.PENDING_BREAK)                                                                                                                                                                
            })                                                                                                                                                                                                
        }                                                                                                                                                                                                     
        throw Error('Bug flies ~~')                                                                                                                                                                           
    }                                                                                                                                                                                                         
}                                                                                                                                                                                                             
CacheManager.MISSED = new Object()                                                                                                                                                                            
CacheManager.PENDING_BREAK = 250                                                                                                                                                                                                                     

SessionCacheProvider.js

class SessionCacheProvider {                                                                                                                                                                                  
    constructor() {                                                                                                                                                                                           
        if (SessionCacheProvider.__default !== null) {                                                                                                                                                        
            throw Error('New operation is forbidden!')                                                                                                                                                        
        }                                                                                                                                                                                                     
    }                                                                                                                                                                                                         
                                                                                                                                                                                                              
    get(key) {                                                                                                                                                                                                
        let item = sessionStorage.getItem(key)                                                                                                                                                                
        if (item !== null) {                                                                                                                                                                                  
            item = CacheItem.of(item)                                                                                                                                                                         
        }                                                                                                                                                                                                     
        return item                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    set(key, cacheItem) {                                                                                                                                                                                     
        sessionStorage.setItem(key, cacheItem)                                                                                                                                                                
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    remove(key) {                                                                                                                                                                                             
        sessionStorage.removeItem(key)                                                                                                                                                                        
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
}                                                                                                                                                                                                             
SessionCacheProvider.__default = null                                                                                                                                                                         
SessionCacheProvider.default = () => {                                                                                                                                                                        
    if (SessionCacheProvider.__default === null) {                                                                                                                                                            
        SessionCacheProvider.__default = new SessionCacheProvider()                                                                                                                                           
    }                                                                                                                                                                                                         
    return SessionCacheProvider.__default                                                                                                                                                                     
}

LocalCacheProvider.js

class LocalCacheProvider {                                                                                                                                                                                    
    constructor() {                                                                                                                                                                                           
        if (LocalCacheProvider.__default !== null) {                                                                                                                                                          
            throw Error('New operation is forbidden!')                                                                                                                                                        
        }                                                                                                                                                                                                     
    }                                                                                                                                                                                                         
                                                                                                                                                                                                              
    get(key) {                                                                                                                                                                                                
        let item = localStorage.getItem(key)                                                                                                                                                                  
        if (item !== null) {                                                                                                                                                                                  
            item = CacheItem.of(item)                                                                                                                                                                         
        }                                                                                                                                                                                                     
        return item                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    set(key, cacheItem) {                                                                                                                                                                                     
        localStorage.setItem(key, cacheItem)                                                                                                                                                                  
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    remove(key) {                                                                                                                                                                                             
        localStorage.removeItem(key)                                                                                                                                                                          
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
}                                                                                                                                                                                                             
LocalCacheProvider.__default = null                                                                                                                                                                           
LocalCacheProvider.default = () => {                                                                                                                                                                          
    if (LocalCacheProvider.__default === null) {                                                                                                                                                              
        LocalCacheProvider.__default = new LocalCacheProvider()                                                                                                                                               
    }                                                                                                                                                                                                         
    return LocalCacheProvider.__default                                                                                                                                                                       
}  

MemoryCacheProvider.js

class MemoryCacheProvider {                                                                                                                                                                                   
    constructor() {                                                                                                                                                                                           
        this.cache = {}                                                                                                                                                                                       
    }                                                                                                                                                                                                         
                                                                                                                                                                                                              
    get(key) {                                                                                                                                                                                                
        let item = this.cache[key]                                                                                                                                                                            
        if (item == null) return null                                                                                                                                                                         
        else return item                                                                                                                                                                                      
    }                                                                                                                                                                                                         
    set(key, cacheItem) {                                                                                                                                                                                     
        this.cache[key] = cacheItem                                                                                                                                                                           
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
    remove(key) {                                                                                                                                                                                             
        delete this.cache[key]                                                                                                                                                                                
        return this                                                                                                                                                                                           
    }                                                                                                                                                                                                         
}                                                                                                                                                                                                             
MemoryCacheProvider.__default = null                                                                                                                                                                          
MemoryCacheProvider.default = () => {                                                                                                                                                                         
    if (MemoryCacheProvider.__default === null) {                                                                                                                                                             
        MemoryCacheProvider.__default = new MemoryCacheProvider()                                                                                                                                             
    }                                                                                                                                                                                                         
    return MemoryCacheProvider.__default                                                                                                                                                                      
}  

helper.js

function memorize(f, timeout, cacheProvider) {                                                                                                                                                                
    var cacheManager = new CacheManager(cacheProvider || MemoryCacheProvider.default(), f.name || String(+new Date()))                                                                                        
    return function() {                                                                                                                                                                                       
        var args = Array.prototype.slice.call(arguments)                                                                                                                                                      
        var argsId = JSON.stringify(args)                                                                                                                                                                     
        var cachedResult = cacheManager.get(argsId)                                                                                                                                                           
        if (cachedResult !== CacheManager.MISSED) return cachedResult                                                                                                                                         
                                                                                                                                                                                                              
        var result = f.apply(null, args)                                                                                                                                                                      
        cacheManager.set(argsId, result, timeout)                                                                                                                                                             
        return result                                                                                                                                                                                         
    }                                                                                                                                                                                                         
}  

总结

后续还要加入失效缓存定时清理、缓存记录大小限制、总体缓存大小限制和缓存清理策略等功能,毕竟作为生产系统,用户不刷新页面持续操作8个小时是常态,若是无效缓存导致内存溢出就得不偿失了。
当然后面重构各业务模块的缓存代码也是不少的工作量,共勉。

转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/14120882.html —— _肥仔John

欢迎添加我的公众号一起深入探讨技术手艺人的那些事!

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!
  

posted @ 2020-12-11 16:05  ^_^肥仔John  阅读(334)  评论(0编辑  收藏  举报