【收藏就会】浏览器WebStorage缓存使用指南

⚠️本文为掘金社区首发签约文章,未获受权禁止转载前端

背景

在咱们网页刷新的时候,页面上全部数据都会被清空。而在一些网站的搜索上,即便是你关闭了浏览器,下次打开时仍是会有数据在页面上,以下图一个简单的搜索记录功能,当用户进行搜索时,全部的记录会被保存起来,不管是刷新仍是重启浏览器,搜索的历史记录依旧显示在页面上。java

image.png

这一系列的需求均可以经过浏览器的存储技术来实现。本篇文章,咱们就来学习下浏览器存储技术中的WebStorage ,全面了解它的基础使用和进阶,以及如何利用这些方法实践两个常见场景。介绍使用方法的同时,我也会以封装一个工具类的方式来统一全部调用方法,学会这一点,可让你在业务开发调用的时候更加方便。web

为何选择WebStorage?

咱们知道,常见的轻量浏览器存储技术包括CookieWebStorage。那么,咱们为何选择WebStorage而不是Cookie呢?面试

首先,WebStorage在使用上相比Cookie更友好,再也不须要刻意封装成一些工具库来对一些常见的操做进行简化的调用,尽管市面上已经有不少成熟的方案帮咱们作了这件事情。chrome

其次,chrome(80+)浏览器默认屏蔽全部三方Cookie已经不是什么值得震惊的事情了,随着这一次改动,Cookie无疑又被斩断了一只有力的手臂。 不了解的小伙伴,强烈安利一手这篇文章,里面很是详细的对其进行了一些分析。当浏览器全面禁用三方 Cookietypescript

除此以外,使用Cookie还须要面临如下问题:bootstrap

  • 存放数据过小,Cookie的存储大小只有4k,若是你须要存储的数据很是多,那么很显然并不可以知足你的需求,且通常没有人这么作。
  • 每次都会携带在HTTP请求头中,会与服务端进行一些交互,当我单纯存储一些本地数据时,很明显会形成性能浪费。

WebStorage在浏览器中的主要功能,就是在客户端进行临时和永久的数据存储,不直接参与服务端的通讯和交互,所以能够很好地避免一些劫持的安全风险。同时,也具有了良好的存储容量,能胜任绝大部份的应用场景,且每一个存储都是挂载在对应的空间当中,彼此独立去管理对应的数据,不会形成串数据和错数据的一些困扰。后端

基于此,若是有须要存储到本地的一些数据,仍是尽量使用WebStorage来作为存储的首要选择。浏览器

基础使用

在本章节,我会从一个封装工具类的角度带你们学习一些webStorage的基础使用技巧。这里也先分享一个在线的源码地址连接:Storage操做封装实践代码缓存

而后,在浏览器调试工具的Application菜单当中,左侧能够看到Storage的调试版,其中就有咱们经过API保存到存储当中的值,能够在这里进行调试。

image.png

环境支持 & 初始化

在开始前第一步确定是须要作一些环境检查,否则在部分不支持这些特性的浏览器下是没法使用的,这个能够在caniuse上查看一些浏览器支持程度。

image.png

而在咱们的代码中,也要加上一层容错判断,若是须要对其作兼容的话能够进行一个处理降解。如Cookie或者是IE6中userData持久化用户数据

下面是一个比较简单的判断,也能够封装成为一个简单的函数来进行调用。若是浏览器不支持则抛出一些错误到控制台当中。

class CustomStorage {
  private readStorage: Storage

  constructor () {
    if (!window) {
        throw new Error('当前环境非浏览器,没法消费全局window实例。')
    }
    if (!window.localStorage) {
        throw new Error('当前环境非没法使用localStorage')
    }
    if (!window.sessionStorage) {
        throw new Error('当前环境非没法使用sessionStorage')
    }
  }
}
复制代码

当环境支持使用WebStorage的条件下,就能够初始化默认的一些数据了,在这里选择使用哪一个Storage,同时将配置保存起来。

interface StorageBootStrapConfig {
  /** 当前环境 */
  mode: 'session' | 'local',
  
  /** 超时时间 */
  timeout: number
}

/** * 初始化Storage的数据 * @param config StorageBootStrapConfig */
 bootStrap (config: StorageBootStrapConfig): void {
  switch (config.mode) {
    case 'session':
      this.readStorage = window.sessionStorage
      break;

    case 'local':
      this.readStorage = window.localStorage
      break;
  
    default:
      throwErrorMessage('当前配置的mode未再配置区内,能够检查传入配置。')
      break;
  }
  this.config = config
}
复制代码

那么,经过bootstrap来初始化当前的一些配置后,在页面里就能够经过当前实例customStorage去使用一些函数方法。

import CustomStorage from 'web-storage-db'

const customStorage = new CustomStorage()

customStorage.bootStrap({
  mode: 'local',
  timeout: 3000,
})

export default customStorage
复制代码

JSON序列化

对于WebStorage来讲,值的存储是很是依赖JSON的序列化。以下图:

image.png

当存入Object类型时,存入的数据会变成其类型的字符串,由于WebStorage的存储只能以字符串的形式存在,因此咱们想要存储引用类型的数据,就须要依赖JSON序列化的能力了。经过stringifyparse等一些方法对值作出处理,就能很好的存储一些引用类型。

可是也有一些JSON.stringify不友好的类型数据,尽可能不要去存储,如undefined, Function, Symbol等等,我在这里也写了一个简单的函数用于检查存储值。

/** * 判断当前值是否可以呗JSON.stringify识别 * @param data 须要判断的值 * @returns 前参数是否能够string化 */
export function hasStringify (data: any): boolean {
  if (data === undefined) {
    return false
  }

  if (data instanceof Function) {
    return false
  }

  if (isSymbol(data)) {
    return false
  }

  return true
}
复制代码

其中isSymbol方法作了一个Symbol类型值的判断。

/** * 判断当前类型是不是Symbol * @param val 须要判断的值 * @returns 当前参数是不是symbol */
export function isSymbol(val: any): boolean {
  return typeof val === 'symbol'
}
复制代码

存入数据

若是须要将数据存储到WebStorage当中,其自己提供一个setItem的API 来作这件事情,在这里以localStorage为例子,能够经过如下形式来存入一个值:

// # 原生
window.localStorage.setItem('key', 'value')

// # attribute形式存储
window.localStorage['key1'] = 'value'
window.localStorage.name = 'wangly19'
复制代码

image.png

而咱们在使用中,显然不会去使用原生API的方式处理,绝大部分都会封装成一个工具方法,来处理一些重复性的工做。就好比在下面的封装中,我就对存储数据的内容作了一层包装,加入了JSON序列化数据过时时间

/** * 设置当前 * @param key 设置当前存储key * @param value 设置当前存储value */
 setItem(key: string, value) {
  if (hasStringify(value)) {
    const saveData: StorageSaveFormat = {
      timestamp: new Date().getTime(),
      data: value
    }
    console.log(saveData, 'saveData')
    this.readStorage.setItem(key, JSON.stringify(saveData))
  } else {
    throwErrorMessage('须要存储的data不支持JSON.stringify方法,请检查当前数据')
  }
}


// 使用
customStorage.setItem('setItem', [1])
复制代码

image.png

读取数据

既然有存入,那么必然会有读取,咱们能够经过getItem或者是Object的形式进行值的读取。下面,咱们就来看看三种方式的实例吧。

image.png

window.localStorage.setItem('person', JSON.stringify({ 
    name: 'wangly19', 
    age: 22 
}))

const person = window.localStorage.getItem('person')


JSON.parse(person)

// { name: "wangly19", age: 22 }
复制代码

image.png

上面是普通的使用方式,而咱们封装时,也会对存入的数据进行一些判断,将存入的JSON数据作一个解析化的处理,直接返回解析后的数据,更加的方便和易于使用。

/** * 获取数据 * @param key 获取当前数据key * @returns 存储数据 */
getItem<T = any>(key: string): T | null {
  const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
  return content?.data || null
}

// # 使用
customStorage.getItem('setItem') // [1]
复制代码

移除

对于存储的移除不只可使用removeItemdelete等操做来对存储中的值进行移除

// # removeItem
window.localStorage.removeItem('person')

// # delete
delete window.localStorage.person
delete window.localStorage['peson']
复制代码

还可使用clear来清除存储中全部的数据。

window.localStorage.clear()
复制代码

此外,若是移除某条数据时Storage没有存储当前key的数据,那么咱们就不须要去执行当前移除数据的操做。咱们来看下面封装的removeItem方法,我加入了一层值是否存在的判断来决定是否是真的须要执行移除这步操做。

/** * 移除一条数据 * @param key 移除key */
removeItem(key: string) {
  if (this.hasItem(key)) {
      this.readStorage.removeItem(key)
  }
}

/** * 清除存储中全部数据 */
clearAll() {
  this.readStorage.clear()
}
复制代码

长度length

WebStorage自带length属性,能够获取当前Storage的长度。

window.localStorage.length

/** * 返回当前存储库大小 * @returns number */
size(): number {
    return this.readStorage.length
}
复制代码

image.png

keys 和 values

看到这里,不少朋友应该知道会怎么实现了吧?没错,经过Object.keysObject.values能够拿到当前Storage中全部的keyvalue。部分埋点SDK会有上报Storage来作数据筛选。

Object.keys(localStorage)
// (4) ["wwwPassLogout", "BIDUPSID", "BDSUGSTORED", "safeIconHis"]
Object.values(localStorage)
// (4) ["0", "30B3EE0AF6EE9F4F89EF16486C288502", "[{\"q\":\"localstorage%20%E8%BF%87%E6%9C%9F%E6%97%B6%…:\"new%20dateshijianchuo\",\"s\":4,\"t\":206989341223}]", ""]
复制代码

其次就是经过key(index)方法,能够直接获取某个位置的值。

window.localStorage.key(0)
window.localStorage.key(1)
window.localStorage.key(2)
复制代码

工具类当中,我也对其进行了封装,可使用getKeys, getValues来获取存储空间的全部KeyValue的集合。

/** * 获取全部key * @returns 回storage当中全部key集合 */
getKeys(): Array < string > {
  return Object.keys(this.readStorage)
}

/** * 获取全部value * @returns 全部数据集合 */
getValues() {
  return Object.values(this.readStorage)
}

// # 使用
customStorage.getKeys()
customStorage.getValues()
复制代码

image.png

是否存在某个属性?

判断当前Storage中是否存在某个属性,不少同窗都是经过getItem去获取一个值,而后判断value是否存在进行一个判断。

可是很显然,咱们可以像操做Object的hasOwnProperty方法来判断当前是否有这个属性,因为返回的是boolean类型,相对来讲更易于理解。

localStorage.key(2)
// "BDSUGSTORED"
localStorage.hasOwnProperty('BDSUGSTORED')
// true
localStorage.hasOwnProperty('1111')
// false
复制代码

image.png

基于此,我也封装了判断存储中是否存在该值的hasItem方法,用于作一些key是否在存储中存在的一些判断。

/** * 判断是否存在该属性 * @param key 须要判断的key */
hasItem(key: string): boolean {
  return this.readStorage.hasOwnProperty(key)
}
复制代码

进阶使用

在进阶使用当中,我会介绍一些工做中可能会碰到的问题,而且给出一些解决方案

过时时间

WebStorageSessionStorage的一个周期是当前会话。而localStorage则若是不手动清除,则不会主动清除存储的数据。

关键词,面试会问:localStorage若是不是主动清除,存储数据是不会过时的。

因此,不少时候若是须要过时时间则须要开发者本身去处理,而处理的方式也很是简单暴力。 那就是给予存储值时带一个时间。参考下面代码,经过new Date().getTime()来取到当前时间,而后设置到存储当中去。

const person = {
    // 存储数据
    data: {
        name: 'wangly19',
        age: 22
    },
    // 过时时间
    timestamp: new Date().getTime()
}
window.localStorage.setItem('person', JSON.stringify(person))
复制代码

获取时间的时候,会进行一个简单的判断,当前时间 - 存储时间 >= 过时时间,这样就可以在值操做的时候作一些判断处理。

// # 原生

let person = localStorage.getItem('person')
person = JSON.parse(val)

// 这里可使用一些库在作处理,如`dayjs`
if(new Date().getTime() - person.timestamp > [过时时间]) {
    // 数据已通过期的一些操做
} else {
    // 正常处理
}
复制代码

所以,须要在原有的getItem的方法上,添加一条过时时间的判断,我也直接封装在函数内处理这一份逻辑。

/** * 获取数据 * @param key 获取当前数据key * @returns 存储数据 */
getItem<T = any>(key: string): T | null {
  const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
  if (content?.timestamp && new Date().getTime() - content.timestamp >= this.config.timeout) {
    this.removeItem(key)
    return null
  }
  return content?.data || null
}
复制代码

监听函数

WebStorage修改时,会触发浏览器storage事件。

而在应用中可使用addEventListener添加一个storage事件对其进行绑定。

而这个触发机制能够看下图。在不一样窗口对storage触发的时候会输出当前的event信息。在event当中,咱们能够拿到触发的url,新值, 旧值, 触发的key等信息,咱们能够经过这个API去作一些浏览器URL监听的事情。

<script>
 document.body.innerHTML = '初始化数据'
 window.addEventListener("storage", function (event) {
 const values = {
 url: event.url,
 key: event.key,
 old: event.oldValue,
 new: event.newValue,
 }
 document.body.innerHTML = JSON.stringify(values)
 });
 </script>
复制代码

修改数据

因为原生没有changeItem这类的方法,所以咱们须要本身去作一些方法的封装来方便咱们频繁的须要去修改存储当中数据。

以下面的一个相似于useState回调的形式来作一些值的修改。

changeItem('name', (oldValue) => {
    const name = `update: ${oldValue} update`
    return name
})
复制代码

实现方式也相对比较易懂,经过getItem先获取数据,而后在经过setItem设置onChange回调函数的值,将一个连贯的操做串联起来。

/** * 修改当前存储内容数据 * @param key 当前存储key * @param onChange 修改函数 * @param baseValue 基础数据 */
 changeItem<S = any>(
  key: string, 
  onChange: (oldValue: S) => S | null, baseValue?: any
) {
  const data = this.getItem<S>(key)
  this.setItem(key, onChange(data || baseValue))
}

// # 使用
customStorage.changeItem('key', (oldValue) => {
    retutn oldValue + 'newUpadte'
})
复制代码

空间 & 溢出

若是是重度使用用户,如一些文档构建项目,每每不少都是会往localStorage中存不少数据,不少开发者都会担忧会不会直接溢出

因此在这里,也设想了一些解决方案来处理这些问题。

存储状态 & StorageEstimate

在安全的上下文和支持的浏览器下,经过StorageEstimate能够获取到当前浏览器的一个缓存状况,如:使用多少, 总共多少。

以下代码,首先判断了浏览器是否存在navigator,而后继续判断了navigator是否有storage,最后再去执行estimate异步获取咱们的存储信息。

if (navigator && navigator.storage) {
    navigator.storage.estimate().then(estimate => {
        console.log(estimate)
    });
}
复制代码

image.png

该Web API须要当前项目在https下。获取到的quota(存储总量)相对来讲在3M左右,在开发场景下,这绝对是一个安全的内存范围。

缓存溢出清理

若是是在内存濒临溢出的场景下,那么咱们就须要释放一些空间来作处理后面的数据修改了。 首先咱们对带有时间的数据进行汇总排序,以下方法就是将storage中全部带有timestamp字段的数据汇总后进行排序。

/** * 获取当前清除存储空间,而且进行排序 */
getClearStorage() {
  const keys: string[] = Object.keys(this.readStorage)
  const db: Array<{
    key: string,
    data: StorageSaveFormat
  }> = []
  keys.forEach(name => {
    const item = this.getItem(name)
    if (item.timestamp) {
      db.push({
        key: name,
        data: item
      })
    }
  })
  return db.sort((a, b) => {
    return a.data.timestamp - b.data.timestamp
  })
}
复制代码

当拥有了一个排序好的数据列表时,就须要考虑数据清空了,按照时间线将距离当前越久的时间清除。而这个时候,须要理解一个条件: 总大小(quota) - (使用大小)usage > [当前存入大小currentSize]

当咱们有一个排序好的存储时,只须要循环判断当前空间是否知足需求便可,若是知足跳出循环。反之继续异步,直到咱们的空间够为止。

initCacheSize单纯对容量数据最一个刷新。获取新的容量数据。

/** * 容量清理,直到知足存储大小为止 */
detectionStorageContext(currentSize: number) {
  if (this.usage + currentSize >= this.quota) {
      const storage = this.getClearStorage()
      for (let { key, data } of storage) {
          // 若是知足要求就跳出,还不够就继续清除。
          if (this.usage + currentSize < this.quota) break
          // 刷新容量大小
          this.removeItem(key)
          initCacheSize()
      }
  }
}
复制代码

最后一步就是在setItem中执行detectionStorageContext, 每次更新存储内容都会先判断下是否要溢出,若是添加或者修改的数据会溢出,那么我就会作一个空间清理了。

实践场景

本章节,主要讲述了一些简单的WebStorage的使用场景。

搜索历史

到这里,咱们的一个工具类就已经基本成型了。最后,再回到一开始的案例中,咱们就能够经过工具类中的changItem迅速的实现这个搜索历史的功能,而没必要关心一些数据兼容上的问题。咱们须要关注的只是存储值的设置。

image.png

事例代码以下:

export default function Search() {
  const [searchList, setSearchList] = useState([]);

  useEffect(() => {
    const data = localStore.getItem('search')
    setSearchList(data || [])
  }, [])

  const onSearch = (value) => {
    if (value) {
      localStore.changeItem(
        'search',
        (oldValue) => {
          if (oldValue.includes(value)) {
            return oldValue;
          }

          if (oldValue) {
            const newValue = [...oldValue, value];
            setSearchList(newValue);
            console.log(newValue, 'value');
            return newValue;
          }

          if (value) {
            setSearchList([value]);
            return [value];
          }

          return [];
        },
        [],
      );
    }
  };

  return (
    <div className="demo-app"> <Search placeholder="请输入搜索内容" enterButton="Search" size="large" suffix={suffix} onSearch={onSearch} /> <div className="tag-wrapper"> {searchList.map((e) => { return ( <Tag key={e} style={{ margin: 10, }} color="#108ee9" > {e} </Tag> ); })} </div> </div>
  );
}
复制代码

图片数据

浏览器对于请求是有限制的,而咱们项目中绝大部份图片实际上是经过后端接口进行返回的,在这里以emoji表情包作个例子。

咱们拿知乎的表情包数据来进行一个模拟,发现一共有73条数据,若是每次刷新网页都请求一次后端数据是一件很是难受的事情,而这些数据显然也不须要存放在Store当中,在必定的时间中,发生改变的概率很小,那么咱们将它放在本地存储显然是一个不错的选择。

image.png

在页面加载时,我会对接口数据请求加一层判断,只有数据为空时才会请求后端图标数据列表。若是是过时时间的话,获取数据时会清空本地图标数据,而后从新请求后端图标数据,在从新放入缓存中而且更新新的过时时间。

const emojiRef = useRef(localStore.getItem('emoji'));

useEffect(() => {
    if (!emojiRef.current) {
      fetchEmojiIcon()
    }
  })
复制代码

若是你项目中存在大量的资源路径,能够将其放在localStorage中进行存储,方便须要用到时进行使用。

image.png

资源 & 资料

总结

本文对WebStorage中绝大部分使用技巧都作了一些使用的总结,将经常使用的一些操做存储方法都进行了封装,同时也对工做中常常碰到的一些复杂场景,如过时时间、数据更改、缓存溢出等功能进行了一些叙述,最后将其封装到了工具类 当中,方便在平常开发中进行调用。

最后在对WebStorage有了一些了解以后,那么咱们在后续工做中,是否是能够思考有些数据能够考虑放到存储当中去?在节省资源的同时,也能有更好的性能,同时也缓解了部分服务端的压力。

近期好文

尾注

若是本文对你有帮助,但愿可以给我点一赞支持一下。

本文首发于:掘金技术社区 类型:签约文章 做者:wangly19 收藏于专栏:javaScript基础进阶 公众号: ItCodes 程序人生

相关文章
相关标签/搜索