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


    前言

    之前系统接入大数据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

  • 相关阅读:
    (6)UIView常见属性二
    linux系统中不间断会话服务screen命令
    linux系统中配置sshd服务(远程控制服务)
    Linux系统中远程传输命令scp
    linux系统中防火墙管理工具iptables
    什么是端口?
    linux系统实现会话共享功能
    linux系统中远程控制服务安全密码验证 sshkeygen命令
    linux系统中防火墙策略管理工具firewalld
    linux系统中nmcli命令、查看、添加、删除、编辑网络会话
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/14120882.html
Copyright © 2020-2023  润新知