技术小哥

16天前 阅读: 52 点赞: 0 评论: 1

浏览器WebStorage缓存使用指南

背景

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

image.png

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

为什么选择WebStorage?

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

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

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

除此之外,使用Cookie还需要面临以下问题:

  • 存放数据太小,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 程序人生

链接:https://juejin.cn/post/6984908770149138446

来源:juejin