前端魔法堂:手写缓存模块
前言
之前系统接入大数据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
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!