一、碎碎念
总觉得自己做项目ts写得很别扭,很多用法都不会也不知从何学起,对于项目结构也是似懂非懂,于是开始看Vben源码,确实看得头皮发麻,但是没办法,还是得一步一步来,希望能坚持看完,刚理解了本地数据存储的封装,确实有学到一些新东西,记录一下。
二、Vben本地数据存储封装思路
我自己在做项目时,存储token之类的数据都是直接操作localStorage,每次要获取就要从localStorage里面取一次,并且每个数据都单独存一个字段,这种做法好像是有那么点问题。在Vben中首先封装了一个Memory类,该类用于记录需要存储在本地的对象,并且在给memory对象赋值时还会设置一个定时器,到期自动移除memory对象中的属性。在存入localStorage或者sessionStorage时使用JSON.stringfy进行序列化成为一个字符串然后存入。每次需要修改storage中内容时先修改对应memory的值,然后整体存入,获取也是获取memory的值。也就是每次都是直接操作memory对象,然后再将对象序列化存入storage中,这样value过期的问题也不用再判断啦。下面上我简化过一些的代码
// Memory类 memory.ts
/**
* time 到期时间戳
* alive 存活时间 seconds
*/
export interface Cache<V = any> {
value?: V
timeoutId?: ReturnType<typeof setTimeout>
time?: number
alive?: number
}
const NOT_ALIVE = 0
export class Memory<T = any, V = any> {
private cache: { [key in keyof T]?: Cache<V> } = {}
private alive: number
constructor(alive = NOT_ALIVE) {
this.alive = alive * 1000
}
get getCache() {
return this.cache
}
setCache(cache: { [key in keyof T]?: Cache<V> }) {
this.cache = cache
}
get(key: keyof T) {
return this.cache[key]
}
// expires传失效日期时间戳
set(key: keyof T, value: V, expires?: number) {
let item = this.get(key)
if (!expires || expires <= 0) {
expires = this.alive
}
if (item) {
if (item.timeoutId) {
clearTimeout(item.timeoutId)
item.timeoutId = undefined
}
item.value = value
item.alive = expires
} else {
item = { value, alive: expires }
this.cache[key] = item
}
const now = new Date().getTime()
item.time = now + item.alive!
item.timeoutId = setTimeout(
() => {
this.remove(key)
},
expires > now ? expires - now : expires
)
}
remove(key: keyof T) {
const item = this.get(key)
Reflect.deleteProperty(this.cache, key)
if (item) {
clearTimeout(item.timeoutId!)
}
}
resetCache(cache: { [key in keyof T]: Cache }) {
Object.keys(cache).forEach((key) => {
const k = key as keyof T
const item = cache[k]
if (item && item.time) {
const now = new Date().getTime()
const expire = item.time
if (expire > now) {
this.set(k, item.value, expire)
}
}
})
}
clear() {
Object.keys(this.cache).forEach((key) => {
const k = key as keyof T
const item = this.cache[k]
item && item.timeoutId && clearTimeout(item.timeoutId)
})
this.cache = {}
}
}
// 封装创建Storage的函数 storageCache.ts
export interface CreateStorageParams {
prefixKey: string
timeout?: Nullable<number>
}
/**
*
* @param timeout Expiration time in seconds
* @param prefixKey 前缀
* @param storage 创建的本地存储类型localStorage或sessionStorage
*/
export const createStorage = (
storage = localStorage,
{ prefixKey = '', timeout = null }: CreateStorageParams
) => {
const WebStorage = class WebStorage {
private storage: Storage
private prefixKey: string
constructor() {
this.storage = storage
this.prefixKey = prefixKey
}
private getKey(key: string) {
return `${this.prefixKey}${key}`.toUpperCase()
}
set(key: string, value: any, expire = timeout) {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: expire ? new Date().getTime() + expire * 1000 : null
})
this.storage.setItem(this.getKey(key), stringData)
}
get(key: string, def: any = null) {
const val = this.storage.getItem(this.getKey(key))
if (!val) return def
try {
const data = JSON.parse(val)
const { value, expire } = data
if (!expire || expire >= new Date().getTime()) {
return value
}
} catch (e) {
return def
}
}
remove(key: string) {
this.storage.removeItem(this.getKey(key))
}
clear() {
this.storage.clear()
}
}
return new WebStorage()
}
// 导出分别创建localStorage和sessionStorage的函数
export const createLocalStorage = (
options: CreateStorageParams = { prefixKey: '', timeout: null }
) => {
return createStorage(localStorage, options)
}
export const createSessionStorage = (
options: CreateStorageParams = { prefixKey: '', timeout: null }
) => {
return createStorage(sessionStorage, options)
}
// 存储数据实操类 persistent.ts
import { Memory } from './memory'
import { DEFAULT_CACHE_TIME } from '@/settings/encryptionSetting'
import {
ROLES_KEY,
TOKEN_KEY,
USER_INFO_KEY,
APP_LOCAL_CACHE_KEY,
APP_SESSION_CACHE_KEY
} from '@/enums/cacheEnum'
import { UserInfo } from '@/types/store'
import { toRaw } from 'vue'
import { createLocalStorage, createSessionStorage } from './storageCache'
interface BasicStore {
[TOKEN_KEY]: string | number | null | undefined
[USER_INFO_KEY]: UserInfo
[ROLES_KEY]: string[]
}
type LocalStore = BasicStore
type SessionStore = BasicStore
type LocalKeys = keyof LocalStore
type SessionKeys = keyof SessionStore
const localMemory = new Memory(DEFAULT_CACHE_TIME)
const sessionMemory = new Memory(DEFAULT_CACHE_TIME)
const ls = createLocalStorage()
const ss = createSessionStorage()
function initPersistentMemory() {
const localCache = ls.get(APP_LOCAL_CACHE_KEY)
const sessionStorage = ss.get(APP_SESSION_CACHE_KEY)
localCache && localMemory.resetCache(localCache)
sessionStorage && sessionMemory.resetCache(sessionStorage)
}
export class Persistent {
static getLocal(key: LocalKeys) {
return localMemory.get(key)?.value
}
static setLocal(
key: LocalKeys,
value: LocalStore[LocalKeys],
immadiate = false
) {
localMemory.set(key, toRaw(value))
// TODO
immadiate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache)
}
static removeLocal(key: LocalKeys, immadiate = false) {
localMemory.remove(key)
immadiate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache)
}
static clearLocal(immadiate = false) {
localMemory.clear()
immadiate && ls.clear()
}
static getSession(key: SessionKeys) {
return sessionMemory.get(key)?.value
}
static setSession(
key: SessionKeys,
value: SessionStore[SessionKeys],
immediate = false
) {
sessionMemory.set(key, toRaw(value))
immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache)
}
static removeSession(key: SessionKeys, immediate = false): void {
sessionMemory.remove(key)
immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache)
}
static clearSession(immediate = false): void {
sessionMemory.clear()
immediate && ss.clear()
}
static clearAll(immediate = false) {
localMemory.clear()
sessionMemory.clear()
if (immediate) {
ls.clear()
ss.clear()
}
}
}
function storageChange(e: any) {
const { key, newValue, oldValue } = e
if (!key) {
Persistent.clearAll()
return
}
if (!!newValue && !!oldValue) {
if (APP_LOCAL_CACHE_KEY === key) {
Persistent.clearLocal()
}
if (APP_SESSION_CACHE_KEY === key) {
Persistent.clearSession()
}
}
}
// 当前页面使用的storage被其他页面修改时触发,符合同源策略,在同一浏览器下的不同窗口,
// 当焦点页以外的其他页面导致数据变化时,如果焦点页监听storage事件,那么该事件会触发,
// 换一种说法就是除了改变数据的当前页不会响应外,其他窗口都会响应该事件
window.addEventListener('storage', storageChange)
initPersistentMemory()
三、一些以前没用过的知识点
(1) 对于timeoutId在ts中如果直接定义为number会报类型错误,我之前都是用window.setTimeout来解决,这次学到了新的写法,但是对于ReturnType还是一知半解
timeoutId: ReturnType<typeof setTimeout>
(2) Reflect
Reflect是ES6为了操作对象而提供的新的API,Reflect对象的设计目的为:将Object对象的一些明显属于语言内部的方法放到Reflect对象上;修改某些Object方法的返回结果,让其变得更合理;让Object操作都变成函数行为。比较常用的有
// Object.defineProperty无法定义时抛出错误,而Reflect.defineProperty返回false
Reflect.defineProperty(target, propertyKey, attributes)
// delete obj[name]
// Reflect.deleteProperty返回boolean,操作成功或者属性不存在返回true
Reflect.deleteProperty(obj, name)
// name in obj
Reflect.has(obj,name)
(3) 监听storage变化
// 当前页面使用的storage被其他页面修改时触发,符合同源策略,在同一浏览器下的不同窗口,
// 当焦点页以外的其他页面导致数据变化时,如果焦点页监听storage事件,那么该事件会触发,
// 换一种说法就是除了改变数据的当前页不会响应外,其他窗口都会响应该事件
window.addEventListener('storage', storageChange)