翻译自:Optimistic UI and Clobbering
什么是乐观 UI?
乐观 UI 是一个前端开发范例,向某 API 发起一个变更请求之后,客户端假定请求是成功的,并乐观的更新 UI。
请看下面 GIF 中的示例。它展示了一个来自数据库的计数器值,并且递增按钮使其递增。左侧的计数器实现了传统的同步 UI,而右侧的那一个实现了乐观 UI。
同步 UI
- 向数据库发起
increment
请求 - 一旦请求成功,UI 上的计数器值被更新
- 如果请求是不成功的,UI 上的计数器值保持不改变
乐观 UI
- 向数据库发起
increment
请求 - 假设响应将成功,UI 上的计数器值被立即更新
- 如果响应成功,UI 被来自成功响应的数据更新
- 如果响应失败,UI 上的计数器值返回之前的状态
你为什么应该使用乐观 UI?
正如你在上面看到的,在两种案例中,成功和失败响应均被优雅处理。唯一的不同是,乐观 UI 似乎更快,不管网络瓶颈。在很多案例中,没有理由去等待一个成功的响应,因为很多请求在生产环境中预期是成功的。如果请求失败,你也有一个恢复机制去回退到原始状态。
乐观 UI 的档案毁损
问题
档案毁损是一个软件工程问题,由于副作用,数据源被覆盖。在乐观 UI 场景下,当 UI 在接二连三的发起多个变更时,档案毁损通常发生,并且变更的乐观 UI 被来自不同变更的响应数据覆盖。
考虑一个场景,UI 元素接二连三从值 1 到值 2 到值 3 变更。你能在下面 GIF 里看到档案损毁问题:
如果你尝试实现乐观 UI 没有考虑档案损毁, UI 将经历以下状态。
- 初始状态
- 数据库值:1
- UI 值:1
- 变更到值(2)初始:
- 数据库值:1
- UI(乐观)值:2
- 变更到值(3)初始:
- 数据库值:1
- UI(乐观)值:3
- 变更到值(2)成功:
- 数据库值:2
- UI(成功响应)值:2
- 变更到值(3)成功:
- 数据库值:3
- UI(成功响应)值:3
这意味着,对于看到 UI 的人,UI 从 1 到 2 到 3 到 2 到 3,这在语义上是错误的。UI 理想上应该从 1 到 2 到 3,就是这样。
这是乐观 UI 的档案损毁问题。
解决方案
我们可以通过在更新 UI 前核查过期数据的方式解决问题。为了实现这,我们把每个变更和唯一可对比的标识符关联起来,例如时间戳或数字。这意味着,随着每个变更,你关联一个标识符(例如数字),比之前变更的关联标识稍微大点。这个标识符应该是你的乐观响应和变更响应的一部分。现在,无论 UI 何时更新,我们仅需要核查新的数据是否有比现存数据更大的变更标识符。在这种方式,我们避免使用陈旧数据更新 UI。
现在,我们知道如何导致档案损毁,当一个值从 1 到 2 到 3 时,让我们重新看下 UI 状态:
-
初始值
- 数据库值:1
- 变更标识符:1252
- UI 值:1
-
变更到值(2)初始:
- 数据库值:1
- 乐观数据的变更标识符:1253
- 现存数据的变更标识符:1252
- UI(乐观)值:2
-
变更到值(3)初始:
- 数据库值:1
- 乐观数据的变更标识符:1254
- 现存数据的变更标识符:1252
- UI(乐观)值:3
-
变更到值(2)成功:
-
数据库值:2
-
成功响应数据的变更标识符:1253
-
现存数据的变更标识符:1254
-
UI(成功响应)值:3
注意 UI 不更新数据来自变更响应,因为变更身份标识符比 UI 数据变更标识符小。
-
-
变更到值(3)成功:
- 数据库值:3
- 成功响应数据的变更标识符:1254
- 现存数据的变更标识符:1254
- UI(成功响应)值:3
正如你看到的,UI 从 1 到 2 到 3。
目标是在更新数据源之前,核查并清除数据,并且他可被用于解决很多档案损毁问题。实现该解决方案的唯一要求是服务端应该支持原子增量及更新。
错误处理
有了档案损毁的解决方案,也很容易处理失败请求。因为,每个 UI 状态带着一个变更标识符,无论何时从服务端收到失败响应,我们都可以回滚到之前的变更标识符?
示例
让我们举一个在 TODO 应用场景下实现该解决方案的示例。
假设你有一个 从 Postgres 的todo
表读数据的 TODO 应用。todo
表通常看起来像这样:
因为我们必须为档案损毁负责,我们将为该表增加另一个叫做 update_mutation_identifier
的字段。
现在,无论何时 todo 不断地从激活到完成到激活更新,流程将看起来像下图。
我已经使用 Postgres (和 Hasura for GraphQL)建立了上述示例。