了解在常用的状态管理库 IndexedDB 之间同步应用状态的最佳做法。
当用户首次加载网站或应用时,构建用于呈现界面的初始应用状态通常需要完成大量工作。例如,有时应用需要先对用户客户端进行身份验证,然后发出几个 API 请求,然后才能获得要在页面上显示的所有数据。
将应用状态存储在 IndexedDB 中是缩短重复访问加载时间的好方法。然后,应用可以在后台与任何 API 服务同步,并采用“过时重新验证”策略,使用新数据延迟更新界面。
IndexedDB 的另一个良好用途是存储用户生成的内容,既可以在上传到服务器之前作为临时存储区,也可以作为远程数据的客户端缓存存储,当然,两者兼有。
但是,在使用 IndexedDB 时,有许多重要事项需要注意,而对于刚开始接触 API 的开发者而言,这些事项可能并不明显。本文将解答常见问题,并讨论在 IndexedDB 中保留数据时一些最重要的注意事项。
确保应用可预测
IndexedDB 的许多复杂性源自您(开发者)无法控制的许多因素。本部分探讨了在使用 IndexedDB 时必须注意的许多问题。
并非所有平台都可以存储在 IndexedDB 中
如果您要存储用户生成的大型文件(例如图片或视频),则可以尝试将它们存储为 File
或 Blob
对象。这适用于某些平台,但在某些平台上会失败。特别是 iOS 上的 Safari,无法将 Blob
存储在 IndexedDB 中。
幸运的是,将 Blob
转换为 ArrayBuffer
并不难,反之亦然。我们非常支持在 IndexedDB 中存储 ArrayBuffer
。
但请注意,Blob
具有 MIME 类型,而 ArrayBuffer
则没有。您需要将类型与缓冲区一起存储才能正确进行转换。
如需将 ArrayBuffer
转换为 Blob
,您只需使用 Blob
构造函数即可。
function arrayBufferToBlob(buffer, type) {
return new Blob([buffer], { type: type });
}
另一个方向稍微涉及一些,是一个异步过程。您可以使用 FileReader
对象将 blob 作为 ArrayBuffer
读取。读取完成后,读取器上会触发一个 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 代码中实现适当的错误处理至关重要。这也意味着,通常最好将应用状态保留在内存中(除了将其存储下来),这样在无痕浏览模式下运行或没有存储空间时界面不会中断(即使某些需要存储空间的应用功能无法正常运行)。
每次创建 IDBDatabase
、IDBTransaction
或 IDBRequest
对象时,您都可以通过为 error
事件添加事件处理程序来捕获 IndexedDB 操作中的错误。
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 存储对象时,必须先创建该对象的结构化克隆,并且结构化克隆过程在主线程上进行。对象越大,阻塞时间就越长。
在规划如何将应用状态保留到 IndexedDB 时,这会带来一些挑战,因为大多数常用的状态管理库(如 Redux)都是通过将整个状态树作为单个 JavaScript 对象进行管理的。
虽然以这种方式管理状态有很多好处(例如,可以使代码易于推理和调试),虽然简单地将整个状态树作为单个记录存储在 IndexedDB 中可能既有吸引力又很方便,但在每次更改之后执行此操作(即使已进行限制/去抖动)会导致主线程发生不必要的阻塞,但会增加发生写入错误或导致浏览器崩溃的可能性,甚至在某些情况下,甚至会导致标签页崩溃。
您应该将整个状态树分解成单独的记录并仅更新实际更改的记录,而不是将整个状态树存储在单个记录中。
如果将图片、音乐或视频等大型内容存储在 IndexedDB 中,也是如此。请使用自己的键存储每个项,而不是存储在更大的对象中,这样您可以检索结构化数据,而无需支付检索二进制文件的开销。
与大多数最佳做法一样,这也是“一刀切”。在无法拆分状态对象并仅写入最小更改集的情况下,将数据分解为子树并仅写入子树比始终写入整个状态树更可取。细微的改进总比没有改进要好。
最后,您应始终衡量所编写的代码对性能的影响。虽然对 IndexedDB 进行少量写入操作的执行效果确实优于大规模写入,但只有当对 IndexedDB 的写入操作实际上会导致耗时较长的任务(阻塞主线程并降低用户体验)时,才有意义。衡量效果非常重要,因为这样您才能了解优化目标
总结
开发者不仅可以在会话之间保持状态,还可以减少重复访问时加载初始状态所需的时间,从而利用 IndexedDB 等客户端存储机制来改善应用的用户体验。
虽然正确使用 IndexedDB 可以显著改善用户体验,但不正确使用 IndexedDB 或无法处理错误情况可能会导致应用崩溃和用户不满。
由于客户端存储涉及许多不受您控制的因素,因此请务必对您的代码进行充分测试并正确处理错误,即使是最初看起来不太可能发生的错误也是如此。