要构建可靠的离线体验,您的 PWA 需要存储管理。在缓存章节中,您了解到缓存存储是在设备上保存数据的一种选择。在本章中,我们将向您展示如何管理离线数据,包括数据持久性、限制和可用工具。
存储
存储不仅与文件和资源有关,还可能包括其他类型的数据。在所有支持 PWA 的浏览器上,以下 API 可用于设备上存储
- IndexedDB:一种用于存储结构化数据和 Blob(二进制数据)的 NoSQL 对象存储选项。
- WebStorage:一种使用本地存储或会话存储来存储键/值字符串对的方法。它在 Service Worker 上下文中不可用。此 API 是同步的,因此不建议用于复杂的数据存储。
- Cache Storage:如缓存模块中所述。
您可以使用受支持平台上的 Storage Manager API 管理所有设备存储。Cache Storage API 和 IndexedDB 为 PWA 提供对持久存储的异步访问,并且可以从主线程、Web Worker 和 Service Worker 访问。两者在使 PWA 在网络不稳定或不存在时可靠运行方面都发挥着至关重要的作用。但您应该在何时使用它们呢?
对于网络资源(您通过 URL 请求访问的内容,例如 HTML、CSS、JavaScript、图片、视频和音频),请使用Cache Storage API。
使用 IndexedDB 存储结构化数据。这包括需要以类似 NoSQL 的方式进行搜索或组合的数据,或其他数据,例如不一定与 URL 请求匹配的用户特定数据。请注意,IndexedDB 并非为全文搜索而设计。
IndexedDB
要使用 IndexedDB,首先打开一个数据库。如果数据库不存在,这将创建一个新数据库。IndexedDB 是一个异步 API,但它采用回调而不是返回 Promise。以下示例使用 Jake Archibald 的 idb 库,这是一个用于 IndexedDB 的小型 Promise 封装器。使用 IndexedDB 不需要辅助库,但如果您想使用 Promise 语法,则可以使用 idb
库。
以下示例创建一个用于保存烹饪食谱的数据库。
创建和打开数据库
要打开数据库
- 使用
openDB
函数创建一个名为cookbook
的新 IndexedDB 数据库。由于 IndexedDB 数据库是版本化的,因此每当您更改数据库结构时,都需要增加版本号。第二个参数是数据库版本。在示例中,它设置为 1。 - 包含
upgrade()
回调的初始化对象被传递给openDB()
。upgrade()
回调函数在首次安装数据库或升级到新版本时调用。此函数是唯一可以执行操作的地方。操作可能包括创建新的对象存储(IndexedDB 用于组织数据的结构)或索引(您想要搜索的索引)。这也是数据迁移应该发生的地方。通常,upgrade()
函数包含一个不带break
语句的switch
语句,以便允许每个步骤根据数据库的旧版本按顺序发生。
import { openDB } from 'idb';
async function createDB() {
// Using https://github.com/jakearchibald/idb
const db = await openDB('cookbook', 1, {
upgrade(db, oldVersion, newVersion, transaction) {
// Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
switch(oldVersion) {
case 0:
// Placeholder to execute when database is created (oldVersion is 0)
case 1:
// Create a store of objects
const store = db.createObjectStore('recipes', {
// The `id` property of the object will be the key, and be incremented automatically
autoIncrement: true,
keyPath: 'id'
});
// Create an index called `name` based on the `type` property of objects in the store
store.createIndex('type', 'type');
}
}
});
}
该示例在名为 cookbook
的数据库中创建一个名为 recipes
的对象存储,并将 id
属性设置为存储的索引键,并基于 type
属性创建另一个名为 type
的索引。
让我们看看刚刚创建的对象存储。在将食谱添加到对象存储并在基于 Chromium 的浏览器上打开 DevTools 或在 Safari 上打开 Web Inspector 后,您应该会看到以下内容
添加数据
IndexedDB 使用事务。事务将操作组合在一起,以便它们作为一个单元发生。它们有助于确保数据库始终处于一致状态。如果您有多个应用副本正在运行,它们对于防止同时写入相同数据也至关重要。要添加数据
- 启动一个
mode
设置为readwrite
的事务。 - 获取您将在其中添加数据的对象存储。
- 使用您要保存的数据调用
add()
。该方法接收字典形式(作为键/值对)的数据并将其添加到对象存储中。字典必须可以使用 结构化克隆进行克隆。如果您想更新现有对象,则可以调用put()
方法。
事务有一个 done
Promise,它在事务成功完成时解析,或者在发生 事务错误时拒绝。
正如 IDB 库文档中所述,如果您要写入数据库,tx.done
表示所有内容都已成功提交到数据库。但是,等待各个操作是有益的,这样您就可以看到导致事务失败的任何错误。
// Using https://github.com/jakearchibald/idb
async function addData() {
const cookies = {
name: "Chocolate chips cookies",
type: "dessert",
cook_time_minutes: 25
};
const tx = await db.transaction('recipes', 'readwrite');
const store = tx.objectStore('recipes');
store.add(cookies);
await tx.done;
}
添加 Cookie 后,该食谱将与其他食谱一起位于数据库中。ID 由 indexedDB 自动设置和递增。如果您运行此代码两次,您将有两个相同的 Cookie 条目。
检索数据
以下是如何从 IndexedDB 获取数据的方法
- 启动事务并指定对象存储或存储以及可选的事务类型。
- 从该事务中调用
objectStore()
。确保您指定了对象存储名称。 - 使用您要获取的键调用
get()
。默认情况下,存储使用其键作为索引。
// Using https://github.com/jakearchibald/idb
async function getData() {
const tx = await db.transaction('recipes', 'readonly')
const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
const value = await store.get([id]);
}
存储管理器
了解如何管理 PWA 的存储对于正确存储和流式传输网络响应尤为重要。
存储容量在所有存储选项(包括 Cache Storage、IndexedDB、Web Storage,甚至 Service Worker 文件及其依赖项)之间共享。但是,可用存储量因浏览器而异。您不太可能耗尽存储空间;某些站点可以在某些浏览器上存储兆字节甚至千兆字节的数据。例如,Chrome 允许浏览器使用高达总磁盘空间的 80%,而单个来源可以使用高达整个磁盘空间的 60%。对于支持 Storage API 的浏览器,您可以了解您的应用仍有多少可用存储空间、其配额及其使用情况。以下示例使用 Storage API 获取估计的配额和使用量,然后计算已用百分比和剩余字节数。请注意,navigator.storage
返回 StorageManager
的实例。有一个单独的 Storage
接口,很容易将它们混淆。
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.`);
}
在 Chromium DevTools 中,您可以通过打开Application 选项卡中的 Storage 部分来查看您网站的配额以及存储使用情况的细分。
Firefox 和 Safari 没有提供摘要屏幕来查看当前来源的所有存储配额和使用情况。
数据持久性
您可以向兼容平台上的浏览器请求持久存储,以避免在不活动或存储压力下自动数据驱逐。如果获得批准,浏览器将永远不会从存储中驱逐数据。此保护包括 Service Worker 注册、IndexedDB 数据库和缓存存储中的文件。请注意,用户始终负责,即使浏览器已授予持久存储,他们也可以随时删除存储。
要请求持久存储,请调用 StorageManager.persist()
。与之前一样,可以通过 navigator.storage
属性访问 StorageManager
接口。
async function persistData() {
if (navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log(`Data persisted: ${result}`);
}
您还可以通过调用 StorageManager.persisted()
来检查当前来源是否已授予持久存储。Firefox 会请求用户许可才能使用持久存储。基于 Chromium 的浏览器会根据 启发式方法来确定内容对用户的重要性,从而授予或拒绝持久性。例如,Google Chrome 的一个标准是 PWA 安装。如果用户在操作系统中安装了 PWA 的图标,则浏览器可能会授予持久存储。
API 浏览器支持
Web Storage
File System Access
Storage Manager