Web Storage API 是浏览器内置的键值对(key/value)存储机制,包括 sessionStoragelocalStorage。两者同属 Storage 接口,方法名相同,只是存储数据的有效期不同。

sessionStorage 的数据在页面会话(page session)期间有效。只要标签页不关闭,刷新页面或页面回退,数据都不消失。如果关闭页面,数据就会消失。

localStorage 的数据更持久,即使关闭页面也不会消失。

出于安全考虑,不同源(origin)之间的 Storage 数据相互独立。

基本用法

localStoragesessionStorage 常用的方法包括:

  • setItem(key, value) 设定键名对应的值
  • getItem(key) 读取键名对应的值,如果为空,返回 null
  • removeItem(key) 移除某个键值对
  • clear() 移除所有键值对

两者只能存储字符串类型的数据,如果你想存储复杂的数据结构,需要提前转换为字符串,比如使用 JSON.stringify() 方法。

它俩还有一个不太常用的方法 key(index),可以返回指定索引的键名。有一个不常用的属性 length,可以获取当前的键值对个数。

因此,如果想读取当前 origin 的所有键名,可以这么写:

function getStorageKeys(storage) {
    return Array(storage.length).fill('').map((_, i) => storage.key(i))
}

getStorageKeys(window.localStorage)
getStorageKeys(window.sessionStorage)

storage 事件

当某个页面的 localStorage 数据发生变化,其他页面window 会监听到 storage 事件。这个事件常用于本地缓存数据的同步。

这里有两点需要注意:

  1. sessionStorage 的数据变化不会触发 storage 事件
  2. 引发 localStorage 数据变化的页面,监听不到本次改变触发的 storage 事件

这个事件的类型是 StorageEvent,它有如下属性:

  • key:受影响的键名
  • newValue:数据新值
  • oldValue:数据旧值
  • storageArea:受影响的 Storage 实例,即 localStorage
  • url:引起数据变化的页面地址
window.addEventListener('storage', (e) => {
    console.log('键名', e.key)
    console.log('新值', e.newValue)
    console.log('旧值', e.oldValue)
    console.log('localStorage', e.storageArea)
    console.log('哪个引发的变化', e.url)
})

数据限额

根据 MDN 的描述,浏览器对于 Web Storage 的数据限额是 10MiB,其中 sessionStorage 占 5MiB,localStorage 占据 5MiB。

注意,上述限额针对的是同源下的数据限额,不同源之间数据隔离,互不影响,不会叠加。

如果数据达到存储上限,继续增加数据,会触发 QuotaExceededError 异常。

如果你对 MDN 的说法存疑,最好动手检验一下。

首先,访问任意页面,启动开发者工具,切换到【控制台】标签。下面的操作均在【控制台】中进行。

使用字符串的 repeat() 方法,生成 1MiB 的字符串常量。

const mib = 'a'.repeat(1024 * 1024)

然后,清空本地缓存,依次手动写入多条缓存数据。观察是否抛出异常。

// 清空本地缓存
localStorage.clear()

localStorage.setItem('1', mib)
localStorage.setItem('2', mib)
localStorage.setItem('3', mib)
localStorage.setItem('4', mib)
localStorage.setItem('5', mib) // <- 进行到这里将抛出异常

为什么还没到 5MiB,刚过 4MiB 就报错了?因为键名也会消耗数据限额,上面的 5 个键名总共占据 5 个字节。

如果想榨干 5MiB 的每一个字节,需要从最后一个 mib 字符串中剔除 5 个键名占用的空间:

localStorage.setItem('5', mib.slice(0, -5)) // 这次正常执行

此后,再增加一个字节,都会触发配额超限的异常 QuotaExceededError

看来,MDN 说的是真的。