翻译自:Optimistic UI and Clobbering

什么是乐观 UI?

乐观 UI 是一个前端开发范例,向某 API 发起一个变更请求之后,客户端假定请求是成功的,并乐观的更新 UI。

Optimistic UI

请看下面 GIF 中的示例。它展示了一个来自数据库的计数器值,并且递增按钮使其递增。左侧的计数器实现了传统的同步 UI,而右侧的那一个实现了乐观 UI。

Optimistic UI example

同步 UI

  1. 向数据库发起increment请求
  2. 一旦请求成功,UI 上的计数器值被更新
  3. 如果请求是不成功的,UI 上的计数器值保持不改变

乐观 UI

  1. 向数据库发起increment请求
  2. 假设响应将成功,UI 上的计数器值被立即更新
  3. 如果响应成功,UI 被来自成功响应的数据更新
  4. 如果响应失败,UI 上的计数器值返回之前的状态

你为什么应该使用乐观 UI?

正如你在上面看到的,在两种案例中,成功和失败响应均被优雅处理。唯一的不同是,乐观 UI 似乎更快,不管网络瓶颈。在很多案例中,没有理由去等待一个成功的响应,因为很多请求在生产环境中预期是成功的。如果请求失败,你也有一个恢复机制去回退到原始状态。

乐观 UI 的档案毁损

问题

档案毁损是一个软件工程问题,由于副作用,数据源被覆盖。在乐观 UI 场景下,当 UI 在接二连三的发起多个变更时,档案毁损通常发生,并且变更的乐观 UI 被来自不同变更的响应数据覆盖。

考虑一个场景,UI 元素接二连三从值 1 到值 2 到值 3 变更。你能在下面 GIF 里看到档案损毁问题:

如果你尝试实现乐观 UI 没有考虑档案损毁, UI 将经历以下状态。

  1. 初始状态
    • 数据库值:1
    • UI 值:1
  2. 变更到值(2)初始:
    • 数据库值:1
    • UI(乐观)值:2
  3. 变更到值(3)初始:
    • 数据库值:1
    • UI(乐观)值:3
  4. 变更到值(2)成功:
    • 数据库值:2
    • UI(成功响应)值:2
  5. 变更到值(3)成功:
    • 数据库值:3
    • UI(成功响应)值:3

这意味着,对于看到 UI 的人,UI 从 1 到 2 到 3 到 2 到 3,这在语义上是错误的。UI 理想上应该从 1 到 2 到 3,就是这样。

这是乐观 UI 的档案损毁问题。

解决方案

我们可以通过在更新 UI 前核查过期数据的方式解决问题。为了实现这,我们把每个变更和唯一可对比的标识符关联起来,例如时间戳或数字。这意味着,随着每个变更,你关联一个标识符(例如数字),比之前变更的关联标识稍微大点。这个标识符应该是你的乐观响应和变更响应的一部分。现在,无论 UI 何时更新,我们仅需要核查新的数据是否有比现存数据更大的变更标识符。在这种方式,我们避免使用陈旧数据更新 UI。

现在,我们知道如何导致档案损毁,当一个值从 1 到 2 到 3 时,让我们重新看下 UI 状态:

  1. 初始值

    • 数据库值:1
    • 变更标识符:1252
    • UI 值:1
  2. 变更到值(2)初始:

    • 数据库值:1
    • 乐观数据的变更标识符:1253
    • 现存数据的变更标识符:1252
    • UI(乐观)值:2
  3. 变更到值(3)初始:

    • 数据库值:1
    • 乐观数据的变更标识符:1254
    • 现存数据的变更标识符:1252
    • UI(乐观)值:3
  4. 变更到值(2)成功:

    • 数据库值:2

    • 成功响应数据的变更标识符:1253

    • 现存数据的变更标识符:1254

    • UI(成功响应)值:3

      注意 UI 不更新数据来自变更响应,因为变更身份标识符比 UI 数据变更标识符小。

  5. 变更到值(3)成功:

    • 数据库值:3
    • 成功响应数据的变更标识符:1254
    • 现存数据的变更标识符:1254
    • UI(成功响应)值:3

正如你看到的,UI 从 1 到 2 到 3。

目标是在更新数据源之前,核查并清除数据,并且他可被用于解决很多档案损毁问题。实现该解决方案的唯一要求是服务端应该支持原子增量及更新。

错误处理

有了档案损毁的解决方案,也很容易处理失败请求。因为,每个 UI 状态带着一个变更标识符,无论何时从服务端收到失败响应,我们都可以回滚到之前的变更标识符?

示例

让我们举一个在 TODO 应用场景下实现该解决方案的示例。

假设你有一个 从 Postgres 的todo表读数据的 TODO 应用。todo表通常看起来像这样:

因为我们必须为档案损毁负责,我们将为该表增加另一个叫做 update_mutation_identifier的字段。

现在,无论何时 todo 不断地从激活到完成到激活更新,流程将看起来像下图。

Simultaneous UI and Database states during subsequent rapid mutations

我已经使用 Postgres (和 Hasura for GraphQL)建立了上述示例。