在浏览器中存储数据的方式有很多种。哪种方案最适合您的需求?
在旅途中,互联网连接可能不稳定或根本无法连接,因此离线支持和可靠的性能是渐进式 Web 应用的常见功能。即使在无线环境非常理想的情况下,明智地使用缓存和其他存储技术也能大幅提升用户体验。您可以通过多种方式缓存静态应用资源(HTML、JavaScript、CSS、图片等)和数据(用户数据、新闻文章等)。但哪种解决方案最好呢?您可以存储多少数据?如何防止其被逐出?
我应该使用什么?
以下是存储资源的一般建议:
- 对于加载应用所需的网络资源,请使用 Cache Storage API(属于 service worker)。
- 对于基于文件的内容,请使用 Origin Private File System (OPFS)。
- 对于其他数据,请使用 IndexedDB(带有 Promise 封装容器)。
所有现代浏览器都支持 IndexedDB、OPFS 和 Cache Storage API。它们是异步的,不会阻塞主线程(但 OPFS 也有同步变体,仅在 Web worker 中可用)。它们可从 window 对象、Web 工作器和服务工作器进行访问,因此可以在代码中的任何位置使用它们。
其他存储机制呢?
浏览器中还有其他几种存储机制,但它们的使用范围有限,并且可能会导致严重的性能问题。
SessionStorage 仅适用于特定标签页,并且作用范围限定为标签页的生命周期。它可能有助于存储少量会话特定信息,例如 IndexedDB 键。使用时应谨慎,因为它是同步的,会阻塞主线程。它的大小限制为大约 5MB,并且只能包含字符串。由于它是特定于标签页的,因此无法从 Web Worker 或 Service Worker 访问。
应避免使用 LocalStorage,因为它会同步并阻塞主线程。它的大小上限约为 5MB,并且只能包含字符串。无法从 Web Worker 或 Service Worker 访问 LocalStorage。
Cookie 有其用途,但不应用于存储。 Cookie 会随每个 HTTP 请求一起发送,因此存储少量数据以上的数据会显著增加每个 Web 请求的大小。它们是同步的,并且无法从 Web 工作人员访问。与 LocalStorage 和 SessionStorage 一样,Cookie 仅限于字符串。
File System Access API 旨在让用户能够读取和修改本地文件系统中的文件。用户必须授予权限,网页才能读取或写入任何本地文件;除非在 IndexedDB 中缓存了文件句柄,否则权限不会跨会话保留。 File System Access API 最适合用于编辑器等用例,在这些用例中,您需要打开文件、修改文件,然后可能将更改保存回文件。
File System API 和 FileWriter API 提供了用于在沙盒文件系统中读取和写入文件的方法。虽然它是异步的,但不建议使用,因为它仅适用于基于 Chromium 的浏览器。
我可以存储多少内容?
简而言之,很多,至少几百兆字节,甚至可能达到数百吉字节或更多。浏览器实现各不相同,但可用存储空间量通常取决于设备上的可用存储空间量。
- Chrome 允许浏览器使用最多 80% 的总磁盘空间。一个来源最多可以使用总磁盘空间的 60%。您可以使用 StorageManager API 来确定可用的最大配额。其他基于 Chromium 的浏览器可能有所不同。
- 在无痕模式下,Chrome 会将来源可使用的存储空间量减少到总磁盘空间的大约 5%。
- 如果用户在 Chrome 中启用了“关闭所有窗口时清除 Cookie 及网站数据”,存储空间配额会大幅减少,最多约为 300MB。
- Firefox 允许浏览器最多使用 50% 的可用磁盘空间。一个 eTLD+1 群组(例如
example.com、www.example.com和foo.bar.example.com)最多可使用 2 GB。您可以使用 StorageManager API 来确定还有多少可用空间。 - Safari(桌面版和移动版)似乎允许大约 1 GB 的内存。达到限制后,Safari 会提示用户,并以 200 MB 为增量提高限制。我找不到任何关于此问题的官方文档。
- 如果 PWA 添加到移动版 Safari 的主屏幕,它会创建一个新的存储容器,并且 PWA 与移动版 Safari 之间不会共享任何内容。当已安装的 PWA 达到配额后,似乎无法申请额外的存储空间。
过去,如果网站存储的数据量超过某个阈值,浏览器会提示用户授予使用更多数据的权限。例如,如果来源使用的空间超过 50MB,浏览器会提示用户允许其存储最多 100MB 的数据,然后在每次达到 50MB 的增量时再次询问。
如今,大多数现代浏览器不会提示用户,而是允许网站使用最多为其分配的配额。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.`);
}
您必须捕获超出配额的错误(请参阅下文)。在某些情况下,可用配额可能会超过实际可用的存储空间量。
检查
在开发期间,您可以使用浏览器的开发者工具检查不同的存储类型,并清除所有存储的数据。
Chrome 88 中新增了一项功能,可让您在“存储”窗格中替换网站的存储空间配额。借助此功能,您可以模拟不同的设备,并在磁盘空间不足的情况下测试应用的运行情况。依次前往应用和存储空间,选中模拟自定义存储空间配额复选框,然后输入任意有效数字来模拟存储空间配额。
在编写本指南时,我编写了一个简单工具,旨在尽可能快速地使用尽可能多的存储空间。这是一种快速实验不同存储机制的方法,可让您了解在用完配额时会发生什么情况。
如何处理用量超出配额的情况?
超出配额后,您应该怎么做?最重要的是,您应始终捕获并处理写入错误,无论是 QuotaExceededError 还是其他错误。然后,根据应用设计,决定如何处理该错误。
例如,删除长时间未访问的内容、根据大小移除数据,或为用户提供选择要删除的内容的方式。
当您超出可用配额时,IndexedDB 和 Cache API 都会抛出名为 DOMError 的 QuotaExceededError。
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
}
};
Cache 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 的浏览器空间不足时,会开始逐出数据,先清除最久未使用的来源的所有网站数据,然后清除下一个来源的数据,直到浏览器不再超出限制。
- 当可用磁盘空间用尽时,Firefox 会开始逐出数据,首先清除最久未使用的来源的所有网站数据,然后清除下一个来源的数据,直到浏览器不再超出限制为止。
- Safari 之前不会逐出数据,但最近对所有可写入存储空间实施了新的七天上限(见下文)。
从 iOS 和 iPadOS 13.4 以及 macOS 上的 Safari 13.1 开始,所有可由脚本写入的存储空间(包括 IndexedDB、Service Worker 注册和 Cache API)都有七天的上限。这意味着,如果用户在 Safari 使用七天后未与相应网站互动,Safari 将从缓存中逐出所有内容。此逐出政策不适用于已添加到主屏幕的已安装 PWA。如需了解完整详情,请参阅 WebKit 博客上的完全屏蔽第三方 Cookie 及更多功能。
存储桶
存储分区 API 的核心思想是授予网站创建多个存储分区的能力,浏览器可以选择独立于其他存储分区来删除每个存储分区。这样,开发者就可以指定逐出优先级,确保不会删除最有价值的数据。
奖励:为何要为 IndexedDB 使用封装容器
IndexedDB 是一种低级 API,使用前需要进行大量设置,这对于存储低复杂性数据来说尤其麻烦。与大多数基于 Promise 的现代 API 不同,它是基于事件的。IndexedDB 的 Promise 封装容器(例如 idb)隐藏了一些强大的功能,但更重要的是,它隐藏了 IndexedDB 库附带的复杂机制(例如事务、架构版本控制)。
奖励:SQLite Wasm
在 Web SQL 被弃用并从 Chrome 中移除后,Google 与热门 SQLite 数据库的维护者合作,基于 SQLite 提供 Web SQL 的替代方案。如需详细了解如何使用,请参阅由 Origin Private File System 支持的浏览器中的 SQLite Wasm。
总结
存储空间有限并提示用户存储更多数据的时代已经过去。网站可以有效地存储运行所需的所有资源和数据。使用 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 的实用工具,可用于验证当前行为。这样,您就可以尽可能多地存储数据,并在浏览器中查看存储空间限制。感谢 François Beaufort 深入研究 Safari,找出其存储限制;感谢 Thomas Steiner 添加了有关源私有文件系统、存储分区、SQLite Wasm 的信息,并在 2024 年进行了整体内容更新。