在浏览器中存储数据有许多不同的选项。哪种模式最适合您的需求?
在移动过程中,互联网连接可能不稳定或不存在,因此离线支持和可靠性能是渐进式 Web 应用中的常见功能。即使在完美的无线环境中,合理使用缓存和其他存储技术也可以显著改善用户体验。您可以通过多种方式缓存静态应用资源(HTML、JavaScript、CSS、图片等)和数据(用户数据、新闻报道等)。但哪种解决方案最好?您可以存储多少数据?如何防止其被逐出?
我应该使用哪种消息?
以下是存储资源的一般建议:
- 对于加载应用所需的网络资源,请使用 Cache Storage API(Service Worker 的一部分)。
- 对于基于文件的内容,请使用源私有文件系统 (OPFS)。
- 对于其他数据,请使用 IndexedDB(带有 promises 封装容器)。
每个现代浏览器都支持 IndexedDB、OPFS 和 Cache Storage API。
它们是异步的,不会阻塞主线程(但 OPFS 也有一个同步变体,仅在 Web Worker 中可用)。它们可从 window 对象、Web worker 和 Service Worker 访问,因此可以在代码中的任何位置使用。
其他存储机制呢?
浏览器中还有其他几种存储机制,但它们的用途有限,并且可能会导致严重的性能问题。
SessionStorage 仅适用于特定标签页,其生命周期与标签页相同。这可能有助于存储少量会话特定信息,例如 IndexedDB 密钥。应谨慎使用,因为它是同步的,会阻塞主线程。它的大小上限约为 5MB,并且只能包含字符串。由于它特定于标签页,因此无法从 Web worker 或 Service Worker 访问。
应避免使用 LocalStorage,因为它是同步的,会阻塞主线程。它的大小限制为大约 5MB,并且只能包含字符串。无法从 Web Worker 或 Service Worker 访问 LocalStorage。
Cookie 有其用途,但不应用于存储。 Cookie 会随每个 HTTP 请求一起发送,因此存储的数据量只要超过少量,就会显著增加每个 Web 请求的大小。它们是同步的,无法从 Web Worker 访问。与 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)最多可使用 2GB。您可以使用 StorageManager API 确定还有多少可用空间。 - Safari(桌面版和移动版)似乎允许大约 1GB。达到上限后,Safari 会提示用户,并以 200MB 为增量提高上限。我找不到任何关于此问题的官方文档。
- 如果将 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 都会抛出一个名为 QuotaExceededError 的 DOMError。
IndexedDB
如果来源已超出其配额,则尝试写入 IndexedDB 将失败。系统会调用事务的 onabort() 处理程序,并传递一个事件。
该事件将在 error 属性中包含 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
}
}
逐出机制如何运作?
网络存储分为两类:“尽力而为”和“永久性”。“尽力而为”意味着浏览器可以在不中断用户的情况下清除存储空间,但对于长期或关键数据而言,其持久性较差。 当存储空间不足时,系统不会自动清除永久性存储空间。用户需要手动清除此存储空间(通过浏览器设置)。
默认情况下,网站的数据(包括 IndexedDB、Cache API 等)属于尽力而为类别,这意味着除非网站已请求永久存储,否则浏览器可能会自行决定逐出网站数据,例如在设备存储空间不足时。
尽力而为的逐出政策如下:
- 当浏览器空间不足时,基于 Chromium 的浏览器将开始逐出数据,首先从最久未使用的来源中清除所有网站数据,然后清除下一个来源,直到浏览器不再超出限制。
- 当磁盘可用空间已满时,Firefox 将开始逐出数据,首先清除最久未使用的源中的所有网站数据,然后清除下一个源中的数据,直到浏览器不再超出限制。
- Safari 之前不会逐出数据,但最近对所有可写存储空间实施了新的 7 天上限(见下文)。
从 iOS 和 iPadOS 13.4 以及 macOS 上的 Safari 13.1 开始,所有脚本可写存储空间(包括 IndexedDB、Service Worker 注册和 Cache API)都有 7 天的上限。这意味着,如果用户不与网站互动,Safari 将在用户使用 Safari 7 天后从缓存中逐出所有内容。此逐出政策不适用于已添加到主屏幕的已安装 PWA。如需了解完整详情,请参阅 WebKit 博客上的全面屏蔽第三方 Cookie 及更多内容。
存储桶
Storage Buckets API 的核心理念是让网站能够创建多个存储桶,浏览器可以选择独立于其他存储桶删除每个存储桶。这样,开发者就可以指定逐出优先级,确保最有价值的数据不会被删除。
额外福利:为什么要为 IndexedDB 使用封装容器
IndexedDB 是一种低级 API,在使用前需要进行大量设置,这对于存储低复杂度数据来说可能尤其痛苦。与大多数基于 promise 的现代 API 不同,它是基于事件的。适用于 IndexedDB 的 idb 等 Promise 封装容器隐藏了一些强大的功能,但更重要的是,隐藏了 IndexedDB 库附带的复杂机制(例如事务、架构版本控制)。
彩蛋:SQLite Wasm
在 Web SQL 被弃用并从 Chrome 中移除后,Google 与热门 SQLite 数据库的维护者合作,提供了基于 SQLite 的 Web SQL 替代方案。 如需详细了解如何使用它,请参阅由源私有文件系统支持的浏览器中的 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 年对内容进行了全面更新。