前端魔法堂:手寫快取模組

^_^肥仔John發表於2020-12-11

前言

之前系統接入大資料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

相關文章