Web Storage

在浏览器中存储数据有多种不同选择。哪种方式最符合您的需求?

随时随地访问互联网连接可能会不稳定或根本不存在,因此离线支持和可靠的性能是 Progressive Web 应用中的常见功能。即使在完美的无线环境中,明智地使用缓存和其他存储技术也可以显著改善用户体验。您可以通过多种方式缓存静态应用资源(HTML、JavaScript、CSS、图片等)和数据(用户数据、新闻报道等)。但哪种解决方案是最好的呢?您可以存储多少内容?如何防止它被逐出?

我应该使用什么?

以下是存储资源的一般建议:

所有现代浏览器都支持 IndexedDB 和 Cache Storage API。 这两者都是异步的,不会阻塞主线程。您可通过 window 对象、Web Worker 和 Service Worker 对其进行访问,因此您可以在代码中的任意位置轻松使用它们。

其他存储机制怎么样?

浏览器中还有其他几种存储机制,但它们用途有限,并且可能会导致严重的性能问题。

SessionStorage 特定于标签页,其范围限定为标签页的生命周期。它对于存储少量会话特定信息(例如 IndexedDB 密钥)可能很有用。应谨慎使用,因为它是同步的,会阻塞主线程。其大小限制为约 5MB,并且只能包含字符串。由于它是标签页特定的,因此无法通过 Web Worker 或 Service Worker 访问。

应避免使用 LocalStorage,因为它是同步的,并且会阻塞主线程。它的大小上限为 5MB,并且只能包含字符串。无法从 Web 工作器或 Service Worker 访问 LocalStorage。

Cookie 有其用途,但不应该用于存储。 Cookie 会随每个 HTTP 请求发送,因此如果存储的数据量超过少量数据,每个网络请求的大小就会显著增加。它们是同步的,无法从 Web Worker 访问。与 LocalStorage 和 SessionStorage 一样,Cookie 仅限于字符串。

File System API 和 FileWriter API 提供了在沙盒化文件系统中读取和写入文件的方法。虽然它是异步的,但不建议使用,因为它仅适用于基于 Chromium 的浏览器

File System Access API 旨在让用户能够轻松读取和修改本地文件系统上的文件。用户必须先授予权限,然后页面才能读取或写入任何本地文件,并且权限不会跨会话保留。

不应使用 WebSQL,而应将现有使用方式迁移到 IndexedDB。几乎所有主流浏览器都已不再支持。W3C 已于 2010 年停止维护 Web SQL 规范,并且未计划进一步更新。

不应使用应用缓存,而应将现有使用情况迁移到 Service Worker 和 Cache API。此功能已弃用,未来将不再支持各个浏览器。

我可以存储多少数据?

简而言之,需要很多,至少数百 MB,可能数百 GB 或更多。虽然浏览器实现各不相同,但可用的存储空间量通常取决于设备上的可用存储空间量。

  • Chrome 最多允许浏览器使用总磁盘空间的 80%。一个来源最多可使用总磁盘空间的 60%。您可以使用 StorageManager API 来确定可用配额上限。其他基于 Chromium 的浏览器可能会有所不同。
    • 在无痕模式下,Chrome 会将源站可以使用的存储空间大小减少到总磁盘空间的大约 5%。
    • 如果用户在 Chrome 中启用了“关闭所有窗口时清除 Cookie 和网站数据”,则存储配额会大幅降低至最多约 300MB。
    • 如需详细了解 Chrome 的实现,请参阅 PR #3896
  • Internet Explorer 10 及更高版本最多可存储 250 MB,并在用量超过 10 MB 时提示用户。
  • Firefox 最多允许浏览器使用 50% 的可用磁盘空间。eTLD+1 组(例如,example.comwww.example.comfoo.bar.example.com最多可能会使用 2GB。您可以使用 StorageManager API 来确定仍有多少可用空间。
  • Safari(桌面版和移动版)似乎允许大约 1GB 空间。达到上限时,Safari 会提示用户,并以 200MB 为增量提高上限。我找不到任何关于此问题的官方文档。
    • 如果将某个 PWA 添加到移动版 Safari 的主屏幕中,这似乎是在创建新的存储容器,该 PWA 与移动版 Safari 之间不会共享任何内容。已安装的 PWA 达到配额后,似乎没有任何方法可以请求额外的存储空间。

过去,如果网站存储的数据超过特定阈值,浏览器会提示用户授予使用更多数据的权限。例如,如果源站使用量超过 50 MB,浏览器会提示用户允许其最多存储 100 MB,并以 50 MB 为增量再次询问。

目前,大多数现代浏览器都不会提示用户,但允许网站用完其分配的配额。例外情况似乎是 Safari,它会在超出存储配额时提示,请求权限以增加分配的配额。如果源站尝试使用超出其分配的配额,则继续尝试写入数据将失败。

如何查看可用的存储空间?

许多浏览器中,您可以使用 StorageManager API 来确定源站可用的存储空间大小以及源站占用的存储空间大小。该日志会报告 IndexedDB 和 Cache API 使用的字节总数,并让您可以计算大致的可用存储空间剩余空间。

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

尚未在所有浏览器中实现 StorageManager,因此您必须在使用前对其进行功能检测。即使可以使用此 API,您仍必须捕获超出配额的错误(见下文)。在某些情况下,可用配额可能会超过实际可用的存储空间量。

检查

在开发过程中,您可以使用浏览器的开发者工具来检查不同的存储类型,并轻松清除所有存储的数据。

Chrome 88 中添加了一项新功能,可让您替换网站在存储窗格中的存储空间配额。借助此功能,您可以模拟不同的设备,并在磁盘可用性低的情况下测试应用行为。依次转到应用 > 存储,选中模拟自定义存储空间配额复选框,然后输入任意有效数字来模拟存储配额。

DevTools Storage 窗格。

在编写本文时,我编写了一个简单的工具来尝试快速使用尽可能多的存储空间。这是一种快速简便的实验方法,可用于实验不同的存储机制,并查看用尽所有配额后会发生什么情况。

如何处理超出配额的问题?

如果超出配额,您应该怎么做?最重要的是,您应该始终捕获和处理写入错误,无论错误是 QuotaExceededError 还是其他错误。然后,根据您的应用设计,决定如何处理它。例如,删除长期无人访问的内容、根据大小移除数据,或者为用户提供一种选择他们要删除的内容的方式。

当您超出可用配额时,IndexedDB 和 Cache API 都会抛出一个名为 QuotaExceededErrorDOMError

IndexedDB

如果来源已超出其配额,尝试向 IndexedDB 写入数据将失败。系统会调用事务的 onabort() 处理程序,并传递一个事件。该事件将在错误属性中包含一个 DOMException。检查错误 name 将返回 QuotaExceededError

const transaction = idb.transaction(['entries'], 'readwrite');
transaction.onabort = function(event) {
  const error = event.target.error; // DOMException
  if (error.name == 'QuotaExceededError') {
    // Fallback code goes here
  }
};

缓存 API

如果来源已超出其配额,尝试写入 Cache API 时,系统会拒绝并返回 QuotaExceededError DOMException

try {
  const cache = await caches.open('my-cache');
  await cache.add(new Request('/sample1.jpg'));
} catch (err) {
  if (error.name === 'QuotaExceededError') {
    // Fallback code goes here
  }
}

逐出是如何运作的?

Web 存储分为两个存储分区,即“尽力而为”和“永久性”。 “尽最大努力”意味着浏览器可以在不干扰用户的情况下清除存储空间,但对于长期数据或关键数据而言,持久性存储不佳。存储空间不足时,系统不会自动清除永久性存储空间。用户需要(通过浏览器设置)手动清除此存储空间。

默认情况下,网站的数据(包括 IndexedDB、Cache API 等)属于“尽力而为”类别,这意味着除非网站已请求永久性存储,否则浏览器可能会自行决定逐出网站数据,例如在设备存储空间不足时。

尽力而为的逐出政策是:

  • 当浏览器空间不足时,基于 Chromium 的浏览器将开始逐出数据,从最近最少使用的源中清除所有网站数据,然后清除,直到浏览器不再超出上限为止。
  • Internet Explorer 10 及更高版本不会逐出数据,但会阻止源站写入更多内容。
  • 当可用磁盘空间填满时,Firefox 将开始逐出数据,从最近最少使用的源中清除所有网站数据,然后清除,直到浏览器不再超出限制为止。
  • Safari 之前不逐出数据,但最近对所有可写存储空间实施了新的 7 天上限设置(见下文)。

从 macOS 上的 iOS、iPadOS 13.4 和 Safari 13.1 开始,所有脚本可写存储空间(包括 IndexedDB、Service Worker 注册和 Cache API)都有 7 天的上限。这意味着,如果用户未与网站互动,Safari 将在使用 Safari 7 天后从缓存中逐出所有内容。此逐出政策不适用于已添加到主屏幕的已安装 PWA。如需了解完整详情,请参阅 WebKit 博客上的完整的第三方 Cookie 屏蔽及其他功能

额外知识:为什么要为 IndexedDB 使用封装容器

IndexedDB 是一个低级别 API,需要在使用之前进行大量设置,存储简单的数据尤其困难。与大多数基于 promise 的现代 API 不同,它是基于事件的 API。IndexedDB 的 Promise 封装容器(如 idb)隐藏了部分强大的功能,但更重要的是,隐藏了 IndexedDB 库附带的复杂机制(例如事务、架构版本控制)。

总结

存储空间有限的时代已经一去不复返,这种时代还会提示用户存储越来越多的数据。网站可以有效地存储运行所需的所有资源和数据。使用 StorageManager API,您可以确定您可以使用多少存储空间,以及已使用了多少存储空间。借助永久性存储空间,除非用户将其移除,否则您可以保护该存储空间免遭逐出。

其他资源

此致

特别感谢 Jarryd Goodman、Phil Walton、Eiji Kitamura、Daniel Murphy、Darwin Huang、Josh Bell、Marijn Kruisselbrink 和 Victor Costan 对本文的审阅。同时感谢北村英二、Addy Osmani 和 Marc Cohen 撰写的原创文章作为参考依据。Eiji 编写了一个名为 Browser Storage Abuser(浏览器存储空间滥用)的实用工具,该工具对于验证当前行为非常有用。它可让您存储尽可能多的数据,并在浏览器中查看存储限制。感谢 Francois Beaufort 研究 Safari,以确定其存储限制。

主打图片由 Guillaume Bolduc 制作,来自 Unsplash 用户。