前言
之前系統接入大資料PV統計平臺,最近因PV統計平臺側伺服器資源緊張,要求各接入方必須快取API呼叫驗證用的Token,從而減少無效請求和服務端快取中介軟體的儲存壓力。
雖然系統部分業務模組都有快取資料的需求,但由於沒有提供統一的前端快取模組,這導致各業務模組都自行實現一套剛好能用的快取機制,甚至還會導致記憶體洩漏。
以兄弟部門這張整改工單作為契機,是時候開發一個系統級的前端快取模組,逐步償還技術負債了。
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)
- 高階函式——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