Skip to content
3.5k
11 minutes

Web存储 ​

Internet 连接在旅途中可能不稳定或不存在,因此离线支持和可靠的性能是渐进式 Web 应用程序的常见功能。即使在完美的无线环境中,审慎使用缓存和其他存储技术也可以显著改善用户体验。有多种方法可以缓存您的静态应用程序资源(HTML、JavaScript、CSS、图像等)和数据(用户数据、新闻文章等)。但是,哪种才是最好的解决方案?您能存储多少?如何防止数据被逐出?

我应该使用什么? ​

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

每个现代浏览器都支持 IndexedDB 和 缓存存储 API。它们都是异步的,不会阻塞主线程。可以从window对象、Web worker 和 service worker 访问它们,从而轻松地在代码中的任何位置使用。

其他存储机制呢? ​

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

SessionStorage 特定于选项卡,其作用范围涵盖选项卡的整个生存期。它对于存储 IndexedDB 键等少量会话特定信息可能很有用。此机制是同步的,会阻塞主线程,因此应谨慎使用。其大小限制约为 5MB,并且只能包含字符串。由于它特定于选项卡,因此无法从 web worker 或 service worker 进行访问。

LocalStorage 是同步的,会阻塞主线程,因此应避免使用。其大小限制约为 5MB,并且只能包含字符串。无法从 web worker 或 service worker 访问 LocalStorage。

Cookie 有其用途,但不应该用于存储。Cookie 随每个 HTTP 请求一起发送,因此只能存储少量数据,数据一多就会显著增加每个 Web 请求的大小。Cookie 是同步的,不能从 Web worker 进行访问。与 LocalStorage 和 SessionStorage 一样,cookie 仅限于字符串。

文件系统 API 和 FileWriter API 提供了在沙盒文件系统中读写文件的方法。虽然它是异步的,但不推荐使用,因为它只能在基于 Chromium 的浏览器中使用。

示例场景

下面是关于你可以如何使用文件系统 API 的几个示例:

  • 有上传器的应用
    • 当你选择一个文件或目录进行上传时,你可以赋值文件到一个本地沙盒并一次上传一个块。
    • 应用可以在一次中断后重新上传,中断可能包括浏览器被关闭或崩溃,连接中断,或电脑被关闭。
  • 视频游戏或其他使用大量媒体资源的应用
    • 应用下载一个或多个大压缩包并在本地将他们解压到一个文件目录中。
    • 应用能在后台预取资源,从而让用户能够进入下一项工作或游戏等级,而不需要等待下载。
  • 音频或照片编辑器使用线下访问或本地缓存(有助于表现和速度)
    • 应用可以分段写入文件(例如只覆盖 ID3/EXIF 标签而不是整个文件)。
  • 线下视频浏览
    • 应用可以下载大文件(>1GB)用于以后浏览。
    • 应用可以访问只下载了部分的文件(因此你可以查看你的 DVD 的第一章,即使应用仍在下载剩余部分,或者当你需要取赶火车而没有完成下载时)。
  • 线下网络邮件客户端
    • 客户端下载附件并在本地存储它们。
    • 客户端缓存附件用于稍后的上传。

文件系统访问 API 旨在使用户能够轻松读取和编辑本地文件系统上的文件。用户必须先授予权限,然后页面才能读取或写入本地文件,并且权限不会跨会话保持。

不应使用 WebSQL,目前使用的应迁移至 IndexedDB。几乎所有主要浏览器都已删除此支持。W3C 已在 2010 年停止维护 Web SQL 规范,而且没有进一步更新的计划。

不应使用 应用程序缓存,目前使用的应迁移至 service worker 和缓存 API。此机制已被弃用,未来将从浏览器中删除该支持。

我可以存储多少? ​

简而言之,很多,至少几百 MB,甚至可能是几百 GB 或更多。浏览器实施各不相同,但可用存储量通常取决于设备上可用的存储量。

  • Chrome 允许浏览器使用多达 80% 的总磁盘空间。一个来源最多可以使用总磁盘空间的 60%。您可以使用 StorageManager API 来确定可用的最大配额。其他基于 Chromium 的浏览器可能允许浏览器使用更多的存储空间。有关 Chrome 实施的详细信息,请参阅 PR #3896。

  • Internet Explorer 10 及更高版本最多可以存储 250MB,并且在使用量超过 10MB 时会提示用户。

  • Firefox 允许浏览器使用多达 50% 的可用磁盘空间。eTLD+1 组(例如 example.com 、www.example.com 和 foo.bar.example.com)最多可以使用 2GB。您可以使用 StorageManager API 来确定还有多少空间可用。

  • Safari(桌面版和移动版)似乎允许 1GB 左右。当达到限制时,Safari 会提示用户,同时以 200MB 的增量增加限制。我找不到有关这方面的任何官方文档。

    • 如果将 PWA 添加到移动 Safari 的主屏幕上,它似乎会创建一个新的存储容器,并且 PWA 和移动 Safari 之间不会共享任何内容。一旦已安装的 PWA 达到配额,似乎就没有任何方法可以请求额外的存储空间。

过去,如果站点存储的数据超过某个阈值,浏览器会提示用户授权使用更多的数据。例如,如果来源的使用量超过 50MB,浏览器会提示用户允许它最多存储 100MB,然后以 50MB 的增量再次询问。

如今,大多数现代浏览器都不会提示用户,而是允许站点最多用完为其分配的配额。Safari 好像是例外,它会在 750MB 时进行提示,请求允许存储最多 1.1GB。如果来源尝试使用的量超过为其分配的配额,则进一步尝试写入数据将失败。

如何检查有多少可用存储? ​

在许多浏览器中,您可以使用 StorageManager API 来确定来源的可用存储量及其目前使用的存储量。它会报告 IndexedDB 和缓存 API 使用的总字节数,还可以计算近似的可用剩余存储空间。

js
if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> 已用字节数。
  // quota.quota -> 最大可用字节数。
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`您已使用可用存储的 ${percentageUsed}%。`);
  const remaining = quota.quota - quota.usage;
  console.log(`您最多可以再写入 ${remaining} 个字节。`);
}

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

INFO

其他基于 Chromium 的浏览器在报告可用配额时可能会考虑可用空间量。Chrome 则不会,它将始终报告实际磁盘大小的 60%。这有助于降低确定已存储的跨来源资源大小的能力。

检查 ​

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

Chrome 88 中增加了一项新功能,可让您在“存储窗格”中覆盖站点的存储配额。此功能使您能够模拟不同的设备,并在磁盘可用性较低的情况下测试应用程序的行为。依次转到应用程序和存储,启用模拟自定义存储配额复选框,然后输入有效数字来模拟存储配额。

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

如何处理超出配额? ​

超出配额时,您该怎么办?最重要的是,您应始终捕获并处理写入错误,无论是 QuotaExceededError 还是其他错误。然后,根据您的应用程序设计,决定如何对其进行处理。例如删除长时间未访问的内容、根据大小删除数据,或为用户提供选择要删除的内容的方法。

当超过可用配额时,IndexedDB 和缓存 API 都会抛出名为 QuotaExceededError 的 DOMError。

IndexedDB ​

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

js
const transaction = idb.transaction(['entries'], 'readwrite');
transaction.onabort = function(event) {
  const error = event.target.error; // DOMException
  if (error.name == 'QuotaExceededError') {
    // 此处为回退代码
  }
};

缓存 API ​

如果来源已超过其配额,则尝试写入缓存 API 将被拒绝并返回 QuotaExceededError DOMException。

js
try {
  const cache = await caches.open('my-cache');
  await cache.add(new Request('/sample1.jpg'));
} catch (err) {
  if (error.name === 'QuotaExceededError') {
    // 此处为回退代码
  }
}

逐出如何运作? ​

Web 存储分为两个存储桶:“最大努力”和“永久”。最大努力意味着浏览器可以在不中断用户的情况下清除存储,但对于长期或关键数据的持久性较差。当存储较少时,不会自动清除永久存储。用户需要手动清除此存储(通过浏览器设置)。

默认情况下,站点的数据(包括 IndexedDB、缓存 API 等)属于最大努力类别,这意味着除非站点已请求永久存储,否则浏览器可能会自行决定逐出站点数据,例如,当设备存储较少时。

最大努力的逐出策略如下:

  • 当浏览器空间不足时,基于 Chromium 的浏览器将开始逐出数据,首先从最近最少使用的来源中清除所有站点数据,然后是下一个,直到浏览器不再超过限制。
  • Internet Explorer 10+ 不会逐出数据,但会阻止来源再写入新增数据。
  • 当可用磁盘空间被填满时,Firefox 将开始逐出数据,首先从最近最少使用的来源中清除所有站点数据,然后是下一个,直到浏览器不再超过限制。
  • Safari 以前并不逐出数据,但最近对所有可写存储实施了新的七天上限(见下文)。

从 iOS 和 iPadOS 13.4 以及 macOS 上的 Safari 13.1 开始,所有脚本可写存储都有七天的上限,包括 IndexedDB、服务工作进程注册和缓存 API。这意味着如果用户不与站点交互,Safari 将在 Safari 使用七天后从缓存中逐出所有内容。此逐出策略不适用于已安装的 PWA(它们已添加到主屏幕上)。有关完整的详细信息,请参阅 WebKit 博客上的完全第三方 Cookie 拦截等。

INFO

您可以为您的网站请求永久存储,以保护关键用户或应用程序数据。

额外存储空间:为什么要对 IndexedDB 使用包装器 ​

IndexedDB 为低级 API,在使用前需要进行大量设置,这对于存储简单数据而言尤其痛苦。与大多数现代基于 Promise 的 API 不同,它基于事件。Promise 包装器(比如适用于 IndexedDB 的 idb)隐藏了一些强大的功能,但更重要的是,隐藏了 IndexedDB 库附带的复杂机制(例如事务、架构版本控制)。

结论 ​

有限存储和促使用户存储越来越多数据的日子已经一去不复返了。站点可以有效地存储它们运行所需的所有资源和数据。使用 StorageManager API,您可以确定有多少空间可供您使用,以及您已经使用了多少。使用永久存储时,除非用户将其删除,否则您可以保护它免遭逐出。

其他资源 ​

鸣谢 ​

特别感谢 Jarryd Goodman、Phil Walton、Eiji Kitamura、Daniel Murphy、Darwin Huang、Josh Bell、Marijn Kruisselbrink 和 Victor Costan 对本文的审阅。感谢 Eiji Kitamura、Addy Osmani 和 Marc Cohen 撰写本文所基于的原始文章。Eiji 编写了一个名为 Browser Storage Abuser 的有用工具,可用于验证当前行为。它允许您存储尽可能多的数据并查看浏览器的存储限制。感谢 Francois Beaufort 对 Safari 的深入研究,找出其存储限制。

主图由 Guillaume Bolduc 在 Unsplash 上创作。


原文