线下数据

为了打造可靠的离线体验,您的 PWA 需要管理存储空间。在缓存章节中,您了解了缓存存储是在设备上保存数据的一种方法。在本章中,我们将介绍如何管理离线数据,包括数据持久性、限制和可用工具。

存储空间

存储不仅仅是文件和资产,还可以包含其他类型的数据。在所有支持 PWA 的浏览器上,以下 API 可用于设备端存储:

  • IndexedDB:适用于结构化数据和 blob(二进制数据)的 NoSQL 对象存储选项。
  • WebStorage:使用本地存储或会话存储来存储键值对字符串对的方法。它在 Service Worker 上下文中不可用。此 API 是同步的,因此不建议将其用于复杂的数据存储。
  • 缓存存储空间:如缓存模块中所述。

您可以在支持的平台上使用 Storage Manager API 管理所有设备存储空间。Cache Storage API 和 IndexedDB 为 PWA 提供了对永久性存储空间的异步访问,并且可以通过主线程、Web 工作器和 Service Worker 进行访问。在网络不稳定或不存在时,两者都有助于 PWA 可靠地运行。但是,分别应该在什么情况下使用?

针对网络资源(您可以通过网址请求访问的内容,例如 HTML、CSS、JavaScript、图片、视频和音频)使用 Cache Storage API

使用 IndexedDB 存储结构化数据。这类数据包括需要可搜索或能以 NoSQL 式方式组合的数据,或者不需要与网址请求匹配的用户特定数据等其他数据。请注意,IndexedDB 不适用于全文搜索。

IndexedDB

如需使用 IndexedDB,请先打开数据库。如果不存在数据库,此操作将创建一个新数据库。 IndexedDB 是一个异步 API,但它接受回调而不是返回 Promise。以下示例使用了 Jake Archibald 的 idb 库,该库是 IndexedDB 的小型 Promise 封装容器。使用 IndexedDB 不需要帮助程序库,但如果要使用 Promise 语法,可以选择 idb 库。

以下示例创建了一个数据库来保存烹饪食谱。

创建和打开数据库

如需打开数据库,请执行以下操作:

  1. 使用 openDB 函数创建名为 cookbook 的新 IndexedDB 数据库。由于 IndexedDB 数据库带有版本编号,因此每当更改数据库结构时,您都需要增加版本号。第二个参数是数据库版本。在本示例中,该值设置为 1。
  2. 包含 upgrade() 回调的初始化对象会传递给 openDB()。该回调函数会在第一次安装数据库或升级到新版本时调用。此函数是唯一可以执行操作的位置。操作可能包括创建新的对象存储(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 的浏览器或 Safari 上的 Web 检查器中打开开发者工具后,您应该会看到以下内容:

Safari 和 Chrome 显示 IndexedDB 内容。

添加数据

IndexedDB 使用事务。事务将操作组合到一起,使这些操作作为一个整体发生。它们有助于确保数据库始终处于一致的状态。如果您正在运行应用的多个副本,那么这些副本也至关重要,可防止同时写入相同的数据。要添加数据,请执行以下操作:

  1. 启动一个将 mode 设置为 readwrite 的事务。
  2. 获取将在其中添加数据的对象存储。
  3. 使用要保存的数据调用 add()。该方法以字典形式接收数据(作为键值对),并将其添加到对象存储。字典必须可以使用结构化克隆进行克隆。如果您想更新现有对象,应调用 put() 方法。

事务有一个 done promise,该 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 由 indexDB 自动设置和递增。如果运行此代码两次,您将得到两个相同的 Cookie 条目。

检索数据

下面展示了如何从 IndexedDB 获取数据:

  1. 启动一个事务并指定一个或多个对象存储,以及(可选)事务类型。
  2. 从该事务中调用 objectStore()。请务必指定对象存储名称。
  3. 使用您想要获得的密钥调用 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 文件及其依赖项。 但是,可用的存储空间容量因浏览器而异。您不太可能用尽存储空间;在某些浏览器上,网站可能会存储 MB 甚至 GB 的数据。例如,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 开发者工具中,您可以打开应用标签页中的存储空间部分,查看网站的配额和存储空间用量(按使用者细分)。

应用的“清除存储空间”部分中的 Chrome 开发者工具

Firefox 和 Safari 未提供用于查看当前源的所有存储配额和使用情况的摘要屏幕。

数据持久性

您可以要求浏览器在兼容的平台上使用永久性存储空间,以免在处于不活动状态或面临存储压力后自动将数据逐出。如果已授予权限,浏览器绝不会从存储空间中逐出数据。这项保护包括 Service Worker 注册、IndexedDB 数据库以及缓存存储空间中的文件。请注意,用户始终由用户掌控,他们可以随时删除所存储的存储空间,即使浏览器授予永久性存储空间亦是如此。

如需请求永久性存储空间,请调用 StorageManager.persist()。与之前一样,StorageManager 接口通过 navigator.storage 属性进行访问。

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

您还可以调用 StorageManager.persisted(),检查当前源中是否已授予永久性存储空间。Firefox 向用户请求获得使用永久性存储空间的权限。基于 Chromium 的浏览器会根据启发法确定内容对用户的重要性,以决定内容是否保留。例如,使用 PWA 安装 Google Chrome 时需要满足一个条件。如果用户在操作系统中安装了 PWA 图标,浏览器可能会授予永久性存储空间。

Mozilla Firefox 请求用户授予存储持久性权限。

API 浏览器支持

Web 存储

浏览器支持

  • 4
  • 12
  • 3.5
  • 4

来源

文件系统访问权限

浏览器支持

  • 86
  • 86
  • 111
  • 15.2

来源

存储空间管理器

浏览器支持

  • 55
  • 79
  • 57
  • 15.2

来源

资源