了解如何在 IndexedDB 和常用状态管理库之间同步应用状态的最佳实践。
当用户首次加载网站或应用时,构建用于呈现界面的初始应用状态通常需要完成大量工作。例如,有时应用需要在客户端对用户进行身份验证,然后发出多个 API 请求,才能获得在页面上显示所需的所有数据。
将应用状态存储在 IndexedDB 中是加快重复访问加载时间的绝佳方式。然后,应用可以与后台的任何 API 服务同步,并采用“在重新验证时使用过时数据”策略,以便使用新数据延迟更新界面。
不过,使用 IndexedDB 时,有许多重要事项需要考虑,这些事项对于刚开始接触这些 API 的开发者来说可能并不明显。本文档将解答常见问题,并讨论在 IndexedDB 中保留应用状态时需要注意的一些最重要的事项。
确保应用可预测
IndexedDB 的许多复杂性源于您(开发者)无法控制的许多因素。本部分将探讨在使用 IndexedDB 时必须注意的许多问题。
写入存储空间的操作可能会失败
写入 IndexedDB 时出错,可能是由各种原因造成的,在某些情况下,这些原因超出了开发者的控制范围。例如,某些浏览器不允许在无痕式浏览模式下写入 IndexedDB。此外,用户使用的设备也有可能磁盘空间几乎用尽,浏览器会限制您存储任何内容。
因此,请务必始终在 IndexedDB 代码中实现适当的错误处理。这也意味着,一般情况下,除了存储应用状态之外,最好还是将应用状态保存在内存中。这样,在无痕浏览模式下运行时或存储空间不可用时,界面不会中断(即使某些需要存储空间的应用功能无法正常运行)。
用户可能修改或删除了存储的数据
与服务器端数据库不同,您无法限制对客户端数据库的未经授权的访问,浏览器扩展程序和开发者工具可以访问客户端数据库,并且用户可以清除这些数据库。
虽然用户修改本地存储的数据的情况可能并不常见,但清除本地存储的数据的情况却很常见。请务必确保您的应用能够处理这两种情况,且不会出错。
存储的数据可能已过期
与上一部分类似,即使用户自己没有修改数据,也可能是他们在存储空间中的数据是由较低版本的代码编写的(可能是版本中存在错误的版本)。
IndexedDB 内置了对架构版本和使用其 IDBOpenDBRequest.onupgradeneeded()
方法升级的支持;但是,您仍然需要以能够处理来自先前版本的用户(包括存在 bug 的版本)的方式编写升级代码。
单元测试在此非常有用,因为通常无法手动测试所有可能的升级路径和用例。
确保应用的性能稳定
IndexedDB 的一项关键功能是其异步 API,但请不要被这点误导,以为在使用它时无需担心性能。在很多情况下,使用不当仍可能阻塞主线程,从而导致无响应。
一般来说,对 IndexedDB 的读写操作不应超过对所访问数据所需的操作。
虽然 IndexedDB 允许将大型嵌套对象存储为单个记录(从开发者的角度来看,这样做确实非常方便),但应避免这种做法。原因在于,当 IndexedDB 存储对象时,必须先创建该对象的结构化克隆,并且结构化克隆过程发生在主线程上。对象越大,阻塞时间就越长。
这在规划如何将应用状态持久化到 IndexedDB 时会带来一些挑战,因为大多数热门状态管理库(例如 Redux)都是通过将整个状态树作为单个 JavaScript 对象进行管理来工作的。
以这种方式管理状态有很多好处,虽然将整个状态树作为单个记录存储在 IndexedDB 中可能很有吸引力,但在每次更改后执行此操作(即使已进行节流/去抖动)会导致主线程发生不必要的阻塞,但会增加写入错误的可能性,在某些情况下,甚至会导致浏览器标签页无响应或无响应。
您应将整个状态树拆分为单独的记录,而不是将其存储在单个记录中,并且只更新实际发生更改的记录。
与大多数最佳实践一样,这并不是一个非此即彼的规则。如果拆分状态对象并只写入最小的变更集不可行,那么将数据分解为子树并仅写入子树仍然比始终写入整个状态树更可取。小幅改进总比不改进好。
最后,您应始终衡量您编写的代码对性能的影响。虽然向 IndexedDB 执行小写入的性能确实优于执行大写入,但只有当应用向 IndexedDB 执行的写入实际上会导致长任务阻塞主线程并降低用户体验时,这一点才重要。请务必进行衡量,这样才能了解优化目标是什么。
总结
开发者可以使用 IndexedDB 等客户端存储机制来改进应用的用户体验,不仅可以跨会话保留状态,还可以缩短重复访问时加载初始状态所需的时间。
虽然正确使用 IndexedDB 可以显著改善用户体验,但不正确使用 IndexedDB 或无法处理错误情况都可能会导致应用崩溃和用户不满。
由于客户端存储涉及许多超出您控制范围的因素,因此请务必对代码进行充分测试并妥善处理错误,即使是最初看起来不太可能发生的错误也是如此。