线下数据

为了打造出色的离线体验,您的 PWA 需要进行存储管理。在缓存章节中,您了解了缓存存储是将数据保存在设备上的一种方式。在本章中,我们将向您展示如何管理离线数据,包括数据持久性、限制和可用工具。

存储

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

  • IndexedDB:一种适用于结构化数据和 blob(二进制数据)的 NoSQL 对象存储选项。
  • WebStorage:一种使用本地存储空间或会话存储空间存储键值对的方法。它不适用于服务工件上下文。此 API 是同步的,因此不适用于复杂的数据存储。
  • 缓存存储:如缓存模块中所述。

您可以在受支持的平台上使用 Storage Manager API 管理所有设备存储空间。Cache Storage API 和 IndexedDB 可为 PWA 提供对永久存储空间的异步访问,并且可从主线程、Web 工作器和服务工作器访问。这两种功能在确保 PWA 在网络不稳定或不存在时可靠运行方面发挥着重要作用。但是,何时使用哪种方法?

使用 Cache Storage API 处理网络资源,即您通过网址请求访问的内容,例如 HTML、CSS、JavaScript、图片、视频和音频。

使用 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() 函数包含 switch 语句,但不包含 break 语句,以便根据数据库的旧版本,让每个步骤有序进行。
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 内容的 Safari 和 Chrome。

添加数据

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 由 indexedDB 自动设置和递增。如果您将此代码运行两次,则会得到两个完全相同的 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 的存储空间对于正确存储和流式传输网络响应至关重要。

存储容量在所有存储选项(包括缓存存储、IndexedDB、Web 存储,甚至服务工件文件及其依赖项)之间共享。 不过,可用的存储空间大小因浏览器而异。您不太可能用尽此类存储空间;网站可以在某些浏览器上存储数兆字节甚至数十亿字节的数据。例如,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 不提供用于查看当前来源的所有存储空间配额和用量的摘要界面。

数据持久性

您可以在兼容的平台上向浏览器请求永久性存储,以避免在闲置或存储压力下自动驱逐数据。如果被授予,浏览器将永远不会从存储空间中驱逐数据。此保护措施涵盖服务工件注册、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 的浏览器会根据启发词语来确定内容对用户的重要性,然后决定是否允许持久化。例如,Google Chrome 的一项标准是 PWA 安装。如果用户已在操作系统中安装 PWA 的图标,浏览器可能会授予永久存储权限。

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

API 浏览器支持

Web 存储

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

文件系统访问

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

存储空间管理器

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

资源