使用 IndexedDB 的最佳实践

了解在常用的状态管理库 IndexedDB 之间同步应用状态的最佳做法。

当用户首次加载网站或应用时,通常需要进行大量的工作 构造用于渲染界面的初始应用状态。例如,有时 应用需要对用户客户端进行身份验证,然后发出几个 API 请求, 在网页上显示所需的数据

将应用状态存储在 IndexedDB 是提高速度的好方法, 重复访问的加载时间。然后,此应用便可在后台与任何 API 服务同步 并使用新数据延迟更新界面 stale-while- revalidate 策略。

IndexedDB 的另一个好用是存储用户生成的内容,既可以作为临时存储, 然后再上传到服务器或作为远程数据的客户端缓存,当然,两者兼有。

不过,在使用 IndexedDB 时, 初学 API 的开发者就能一目了然地看到本文将解答一些常见问题, 讨论在 IndexedDB 中保留数据时需要注意的一些最重要的事项。

确保应用可预测

IndexedDB 的许多复杂性源于一个事实, 无法控制本部分探讨了您必须注意的许多问题 使用 IndexedDB 时。

并非所有平台都可以存储在 IndexedDB 中

如果您要存储由用户生成的大型文件(如图片或视频),则可以尝试 将其保存为 FileBlob 对象。这适用于某些平台,但在某些平台上会失败。Safari 已开启 特别是 iOS,无法将 Blob 存储在 IndexedDB 中。

幸运的是,将 Blob 转换为 ArrayBuffer 并不难,反之亦然。存储 IndexedDB 中的 ArrayBuffer 得到了很好的支持。

但请注意,Blob 具有 MIME 类型,而 ArrayBuffer 则没有。您需要 将类型与缓冲区一起存储,以便正确进行转换。

如需将 ArrayBuffer 转换为 Blob,您只需使用 Blob 构造函数即可。

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

另一个方向稍微涉及一些,是一个异步过程。您可以使用 FileReader 对象,用于以 ArrayBuffer 的形式读取 blob。读取完成后,loadend 事件在读取器上触发。您可以将此过程封装在 Promise 中,如下所示:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

写入存储空间可能会失败

写入 IndexedDB 时出错的原因可能有很多种,在某些情况下, 这些原因超出了作为开发者的控制范围例如,某些浏览器目前不支持 在无痕浏览模式下向 IndexedDB 写入数据。 还有一种可能是用户所使用的设备磁盘可用空间即将用尽, 浏览器将完全限制您存储任何内容。

因此,务必要始终在 IndexedDB 代码。这也意味着,通常最好将应用状态保存在内存中( 因此,在无痕浏览模式下运行或在 存储空间不足(即使某些需要存储空间的应用功能 工作)。

您可以通过为 error 事件添加事件处理脚本来捕获 IndexedDB 操作中的错误 每次创建 IDBDatabaseIDBTransactionIDBRequest 对象时。

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

用户可能修改或删除了存储的数据

与您可以限制未经授权的访问的服务器端数据库不同,客户端数据库 可供浏览器扩展程序和开发者工具访问,也可以被用户清除。

虽然用户可能不常见的修改其本地存储的数据,但用户却很常见 将其清除。您的应用必须能够处理这两种情况而不会出错,这一点非常重要。

存储的数据可能已过期

与上一部分类似,即使用户自己没有修改数据, 可能导致存储中的数据由旧版代码(可能是 这个版本包含有错误

IndexedDB 内置了对架构版本和通过其 IDBOpenDBRequest.onupgradeneeded() 方法;不过,您仍然需要在编写升级代码时使其能够处理用户 (包括存在 bug 的版本)。

此时,单元测试会很有帮助,因为手动测试所有可能的 升级路径和支持请求

保持应用性能

IndexedDB 的主要功能之一是其异步 API,但别被它欺骗了 认为您在使用时无需担心性能。在很多情况下 使用不当仍可能会阻塞主线程,从而导致卡顿和无响应。

一般来说,对 IndexedDB 的读写操作不应超过数据所需的大小 资源。

尽管 IndexedDB 可以将大型嵌套对象存储为单个记录(这种做法 不可否认,对开发者而言非常方便),应避免这种做法。通过 因为 IndexedDB 存储对象时,必须先创建一个 结构化克隆 并且结构化克隆过程会在主线程上进行。越大 对象,屏蔽时间就会越长。

在规划如何将应用状态保存到 IndexedDB 时,这带来了一些挑战, Redux 等常用状态管理库通过管理 作为单个 JavaScript 对象处理的整个状态树。

以这种方式管理状态有很多好处(例如,它使代码易于推断和 调试),同时将整个状态树以单条记录的形式存储在 IndexedDB 中, 这很有诱惑力,也很方便,每次更改之后都执行此操作(即使受到限制/去抖动)会导致 不必要的阻塞,这将增加写入错误的可能性,并且 在某些情况下,甚至会导致浏览器标签页崩溃或无响应。

不要将整个状态树存储在单个记录中,而应将其拆分为单独的 并仅更新实际更改的记录。

如果将图片、音乐或视频等大型内容存储在 IndexedDB 中,也是如此。储存各种商品 而不是在更大的对象内,这样您便可以检索结构化数据 而无需支付检索二进制文件的费用。

与大多数最佳做法一样,这也是“一刀切”。在 拆分状态对象,只写入最小的更改集,将数据分解成多个子树 但仅编写那些与始终写入整个状态树相比更可取。很少 改进总比不做任何改进要好。

最后,您应始终衡量对效果的影响 代码结构确实,少量写入 IndexedDB 会比写入大量 IndexedDB 效果更好 这只有在您的应用执行的 IndexedDB 写入确实会导致 长任务 会阻塞主线程并降低用户体验。衡量效果很重要 了解优化目标

总结

开发者可以利用 IndexedDB 等客户端存储机制来改善 不仅可以让应用在多个会话中保持状态 重复访问时加载初始状态所用的方法。

虽然正确使用 IndexedDB 可以极大地改善用户体验,但如果使用不当, 未能处理错误情况可能会导致应用崩溃和用户不满。

由于客户端存储涉及许多超出您控制的因素,因此您的代码必须 并妥善处理错误,即使是最初看起来不太可能发生的错误。