前言
在开发过程中很多场景都需要用到前端把数据缓存在端上的能力
- 业务枚举、标签、配置信息
- 使用应用期间产生的应用/配置数据
- 单用户基础信息
- 根据用户隔离的缓存数据
- 随业务活动增长的数据缓存
- 特殊场景的二进制、媒体数据
前端开发者常常会选择用localStorage
来存储需要缓存到前端的数据,但是并不是所有的场景都适合用这个来管理缓存,滥用还会导致因缓存数据动作失败产生线上问题。
针对不同业务场景,我们应该选择不同的方案去缓存数据。本文就针对最常见的存储方案和场景做一些分类和介绍一些在 Vue/React
中的高阶用法,助力前端开发体验和应用的稳定性。同时,缓存键的命名和管理也是值得关注的。
前端缓存方案
除了 cookie
,我们一般会有以下几种选择:
特性 | sessionStorage | localStorage | indexedDB |
---|---|---|---|
持久时效 | 当前会话期 | 永久 | 永久 |
存储空间 | 5MB | 5MB | 依赖可用磁盘空间 |
同步/异步 | 同步 | 同步 | 异步 |
针对不同的特性我们可以得知存储到sessionStorage
和localStorage
中的数据由于存储空间有限都应符合以下特性:
- 缓存数据条目必须可控,不可存储累积性/爆发性增长性质的数据
- 单个条目数据不可过大,因此不适合存储二进制类的数据(例如大文本、图片、音视频等文件)
而对于indexedDB
,则更适合存储大文件和数据条目跟随业务操作增长的场景。需要注意的是,存取操作都是异步的。
确定不同场景缓存方案
- 针对业务枚举、标签类的,这类的信息往往都是字典数据,数据量不大并且更新不频繁,更新前后改动也不大,这类信息是可以存储到
localStorage
中的 - 使用应用期间产生的应用/配置数据:这个数据量不大的情况下就可以使用
sessionStorage
,否则应该考虑其他状态管理方案,比如pinia - 单用户的基础信息这类信息一般情况是用户在登陆成功之后后端返回的信息,这类信息条目确定,也适合存储到
localStorage
- 根据用户隔离的缓存数据:这个如果用
localStorage
就不符合我们说的数据条目必须可控原则,应该存储到indexedDB
中 - 随业务活动增长的数据缓存:这个毋庸置疑应该选择使用
indexedDB
,localStorage
迟早会爆 - 特殊场景的二进制、媒体数据:这个也应该选择使用
indexedDB
规范
在一个应用中对于所有缓存的key都应该集中管理,数量多了之后要做分级管理,用枚举来管理,避免随处用随处起名的坏习惯。
我们可以单独把项目使用到的常量单独维护, 从一个出口暴露出去
├── src
│ ├── modules
│ │ ├── constant
│ │ │ └── cache.ts // 缓存相关
│ │ │ └── index.ts // 出口
举个子:
// cache.ts
export enum CacheKeyEnum {
USER_INFO = 'user-info',
// ...
}
// index.ts
export * from './cache'
// 使用
import { CacheKeyEnum } from '~/modules/constant'
function getUserCacheInfo() {
return localStorage.getItem(CacheKeyEnum.USER_INFO)
}
这样在项目中统一管理key
会让项目更易于维护。
在项目中更简易、优雅的操作缓存
Vue
得益于@vueuse,我们可以用useStorage
方法把对sessionStorage
和localStorage
的操作简化并且和Vue
的响应系统结合:
第一个参数为缓存的key
,对应setItem
的第一个参数,第二个参数为初始数据,返回值为一个Ref
类型的对象,可以直接操作state.value = { hello: "hello", greetinf: "Hi" }
响应式更改缓存中的值,下面是一些文档中的例子:
import { useStorage } from '@vueuse/core'
// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })
// bind boolean
const flag = useStorage('my-flag', true) // returns Ref
// bind number
const count = useStorage('my-count', 0) // returns Ref
// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref
// delete data from storage
state.value = null
React
同样的在React中也有类似的库react-use,其中也有useLocalStorage,对应的用法:
import {useLocalStorage} from 'react-use';
const Demo = () => {
const [value, setValue] = useLocalStorage('my-key', 'foo');
return (
Value: {value}
);
};
useLocalStorage(key);
useLocalStorage(key, initialValue);
useLocalStorage(key, initialValue, raw);
key
—localStorage
键来管理。initialValue
— 要设置的初始化值,如果localStorage
中的值为空。raw
—boolean
,如果设为true
,钩子将不会尝试JSON
序列化存储的值。
简化indexedDB的操作
说了sessionStorage
和localStorage
的优雅用法,也来说说如何优雅的使用indexedDB
key-value方式的使用
你可以简单理解为localStorage
的异步版本、可以存储大体积数据的版本
有这样一个库可以帮我们简化这个操作:localForage
使用方法也很简单:
import localForage from "localforage";
// 你的项目ID
const PID = "your project's ID"
// 实例
let lfInstance: LocalForage
/** 初始化LF */
export function initLFInstance(): LocalForage {
if (!lfInstance)
lfInstance = localforage.createInstance({ name: PID })
return lfInstance
}
/**
* 设置或读取缓存信息, null: 删除,传参:更新,不传参:获取
* @param key storage key
* @param data storage data
* @returns storage data | null
*/
export function useIndexedDB(key: K, data?: T | null): Promise {
if (!lfInstance) {
console.error('lfInstance is not initialized')
return Promise.resolve(null)
}
else {
return data === null
? (lfInstance!.removeItem(key) as unknown as Promise)
: data === undefined
? lfInstance!.getItem(key)
: lfInstance!.setItem(key, data)
}
}
在应用启动的时候调用一次initLFInstance
方法,之后就可以用封装好的useIndexedDB
方法来操作indexedDB
了
SQL级别的存储数据
这种可以直接用原生的写法也可以考虑用Dexie.js来操作,会方便很多,比如前面说的切换用户数据的缓存我们就可以用这个来处理:
构建:
import type { Table } from 'dexie'
import Dexie from 'dexie'
import { IndexedDBKeys } from '../constant'
// 用户数据表类型
interface IUserData {
id?: number
name: string
phone: string
age: number
}
export class AppDataDexie extends Dexie {
userDataDB!: Table
constructor() {
super(IndexedDBKeys.UserData)
this.version(1).stores({
userDataDB: '++id, name, phone, age',
})
}
}
/** 构建DB实例 */
const db = new AppDataDexie()
/** 导出用户db操作 */
export const userDB = db.userDataDB
使用
import { userDB } from './db'
userDB.add({ id: 1, name: 'senar', phone: 'xxxx', age: 22 })
// ...更多操作可以查看文档
// https://dexie.org/docs/Tutorial/Getting-started
其他一些边界检测
我们前面说了sessionStorage
和localStorage
是有5M
容量限制的,我们如何知道用户的使用情况呢?
// 检测localStorage使用空间
function sieOfLS() {
return Object.entries(localStorage).map(v => v.join('')).join('').length;
}
// 检测sessionStorage使用空间
function sieOfSS() {
return Object.entries(sessionStorage).map(v => v.join('')).join('').length;
}
indexedDB
也是可以检测容量的,可能在safari
浏览器上有兼容性问题,大家也可以参考下:
export async function getCacheUsage() {
const { quota, usage } = await navigator.storage.estimate()
const remainingSpace = quota! - usage!
const unit = 1073741824
const text = `已使用:${(usage ?? 0) / unit} GB, 剩余可用缓存空间:${remainingSpace / unit} GB`
console.info(text)
return remainingSpace
}
总结
前端缓存的选型需要贴合业务场景来选择,大家也可以交流分享下自己遇到过的经典场景,看使用哪种方案、如何设计比较好