前端数据缓存技术选型及应用技巧

封面-前端数据缓存技术

前言

在开发过程中很多场景都需要用到前端把数据缓存在端上的能力

  • 业务枚举、标签、配置信息
  • 使用应用期间产生的应用/配置数据
  • 单用户基础信息
  • 根据用户隔离的缓存数据
  • 随业务活动增长的数据缓存
  • 特殊场景的二进制、媒体数据

前端开发者常常会选择用localStorage来存储需要缓存到前端的数据,但是并不是所有的场景都适合用这个来管理缓存,滥用还会导致因缓存数据动作失败产生线上问题。

针对不同业务场景,我们应该选择不同的方案去缓存数据。本文就针对最常见的存储方案和场景做一些分类和介绍一些在 Vue/React 中的高阶用法,助力前端开发体验和应用的稳定性。同时,缓存键的命名和管理也是值得关注的。

前端缓存方案

除了 cookie,我们一般会有以下几种选择:

特性 sessionStorage localStorage indexedDB
持久时效 当前会话期 永久 永久
存储空间 5MB 5MB 依赖可用磁盘空间
同步/异步 同步 同步 异步

针对不同的特性我们可以得知存储到sessionStoragelocalStorage中的数据由于存储空间有限都应符合以下特性:

  1. 缓存数据条目必须可控,不可存储累积性/爆发性增长性质的数据
  2. 单个条目数据不可过大,因此不适合存储二进制类的数据(例如大文本、图片、音视频等文件)

而对于indexedDB,则更适合存储大文件和数据条目跟随业务操作增长的场景。需要注意的是,存取操作都是异步的。

确定不同场景缓存方案

  • 针对业务枚举、标签类的,这类的信息往往都是字典数据,数据量不大并且更新不频繁,更新前后改动也不大,这类信息是可以存储到localStorage中的
  • 使用应用期间产生的应用/配置数据:这个数据量不大的情况下就可以使用sessionStorage,否则应该考虑其他状态管理方案,比如pinia
  • 单用户的基础信息这类信息一般情况是用户在登陆成功之后后端返回的信息,这类信息条目确定,也适合存储到localStorage
  • 根据用户隔离的缓存数据:这个如果用localStorage就不符合我们说的数据条目必须可控原则,应该存储到indexedDB
  • 随业务活动增长的数据缓存:这个毋庸置疑应该选择使用indexedDBlocalStorage迟早会爆
  • 特殊场景的二进制、媒体数据:这个也应该选择使用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方法把对sessionStoragelocalStorage的操作简化并且和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<boolean>

// bind number
const count = useStorage('my-count', 0) // returns Ref<number>

// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>

// 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 (
    <div>
      <div>Value: {value}</div>
      <button onClick={() => setValue('bar')}>bar</button>
      <button onClick={() => setValue('baz')}>baz</button>
    </div>
  );
};
useLocalStorage(key);
useLocalStorage(key, initialValue);
useLocalStorage(key, initialValue, raw);
  • keylocalStorage 键来管理。
  • initialValue — 要设置的初始化值,如果localStorage中的值为空。
  • rawboolean,如果设为 true,钩子将不会尝试 JSON 序列化存储的值。

简化indexedDB的操作

说了sessionStoragelocalStorage的优雅用法,也来说说如何优雅的使用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<T, K extends string>(key: K, data?: T | null): Promise<T | null> {
  if (!lfInstance) {
    console.error('lfInstance is not initialized')
    return Promise.resolve(null)
  }
  else {
    return data === null
      ? (lfInstance!.removeItem(key) as unknown as Promise<null>)
      : 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<IUserData>

  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

其他一些边界检测

我们前面说了sessionStoragelocalStorage是有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
}

总结

前端缓存的选型需要贴合业务场景来选择,大家也可以交流分享下自己遇到过的经典场景,看使用哪种方案、如何设计比较好

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容