在让 Agent 修改界面前,说清加载、取消与迟到结果
本文目录
用一组有顺序的事件描述异步界面行为:用户选了什么、哪个请求属于这次选择,以及结果到达时屏幕应显示什么。还要包括一种情况:旧请求尚未结束,用户就再次回到了同一个项目。
这样,编程 Agent 才有一份可检查的行为契约。“修一下加载动画”没有说明哪个结果可以替换页面、哪个错误可以显示,也没有说明加载指示应在何时停止。
选定一个具体交互
设想一个虚构的项目预览面板。选择项目后,应用开始只读的预览工作。在这个例子中,每次选择都会发起新请求,不会隐式复用缓存。新的选择会清空上一次预览,并显示包含所选项目名称的加载提示。
下面六个事件是原创的示意需求,不是已复现的缺陷或执行日志。我们特意设定:应用请求取消后,旧的应用层完成事件仍然到达。这里没有提供请求实现,也没有声称被中止的 fetch 一定会在之后成功完成。
写出六个事件的时间线
| 事件 | 发生了什么 | 当前视图归属 | 预期可见状态 |
|---|---|---|---|
| E1 | 用户选择 A;请求 p1 启动 | A / p1 | 正在加载 A;尚无预览 |
| E2 | 用户选择 B;请求 p2 启动 | B / p2 | 清空 A 的预览区域;显示正在加载 B |
| E3 | 应用请求取消旧 p1;尚未确认工作已停止 | B / p2 | 保持正在加载 B;不显示 B 的错误,也不清除其加载状态 |
| E4 | p2 先于 p1 完成 | B / p2 | 显示 B 的预览;B 不再加载 |
| E5 | 用户再次选择 A;新请求 p3 启动 | A / p3 | 清空 B 的预览区域;显示 p3 正在加载 A |
| E6 | 旧 p1 的完成事件到达 | A / p3 | 对当前可见视图忽略 p1;保持 p3 正在加载 A |
完成顺序是 p2 在 p1 之前。在 E6,只检查当前选中的项目是不够的:旧 p1 和新 p3 都对应 A。
结果必须属于当前请求,不能只属于一个同名项目。在这个案例中,只有完成事件与当前项目、当前请求都匹配,而且该请求仍允许完成视图更新时,结果才能填入面板。
区分“取消”的三种含义
MDN 的 AbortController 文档介绍了通过 AbortSignal 与异步工作通信,以及中止受支持的浏览器操作。它没有替这个面板决定产品行为。这里的可见状态规则是本案例选定的设计。
| 动作 | 它可以表达的要求 | 不能据此假定什么 |
|---|---|---|
| 请求取消 | 通过相关操作支持的机制,请求其停止 | 所有产生结果的工作都已停止,或服务器端改动已被撤销 |
| 丢弃结果 | 阻止过时的完成事件修改当前视图 | 产生结果的一方没有做过工作,或远端操作从未完成 |
| 撤销已完成的服务器动作 | 执行另一项受支持的操作,恢复某种状态 | 浏览器中止或忽略响应会自动完成这种撤销 |
这个面板是只读的。如果真实请求会保存或修改远端数据,应另外明确该操作的状态和撤销契约。隐藏结果是显示层的决定,不是其效果已被撤销的证据。
为每种状态定义归属
| 到来的动作或事件 | 归属条件 | 预期视图行为 |
|---|---|---|
| 开始一次选择 | 新的当前项目/请求组合 | 清空之前的预览,显示带项目名称的加载状态 |
| 当前请求成功 | 与当前组合相同,且仍等待完成 | 显示它的预览,结束它的加载状态 |
| 当前请求失败 | 与当前组合相同,且仍等待完成 | 显示该请求的错误,结束它的加载状态 |
| 请求取消一个旧请求 | 该请求已不再拥有当前视图 | 保持当前请求的状态不变 |
| 用户停止当前请求 | 明确将该请求标记为不再允许完成视图更新 | 为所选项目显示“预览已停止”;不装入结果 |
| 当前请求被停止后,完成事件到达 | 已停止的请求不再允许更新视图 | 保持停止状态,不重新填入面板 |
| 旧成功或旧错误 | 不属于当前等待完成的视图 | 不替换内容、不显示其错误,也不停止当前加载动画 |
| 旧清理通知 | 属于已过时的请求 | 不清除另一个请求的加载状态 |
“预览已停止”描述的是选定的界面状态。如果尚未确认产生结果的工作已停止,就不能把这个标签变成所有后台工作都已停止的承诺。
这些规则也覆盖迟到的失败。不能仅仅因为用户又在查看 A,就把 p1 的错误显示成 p3 的失败。
明说策略选择,不把它藏起来
不同产品可以合理地采用不同做法。本例明确选择如下:
| 决策 | 另一种做法 | 选定的策略与理由 |
|---|---|---|
| 新请求期间如何处理旧内容 | 保留旧内容,并明确标为上一次结果 | 清空旧内容,让这个紧凑面板只对应一个可见归属 |
| A 正在停止时是否启动 B | 等待停止确认 | 不等待就启动 B,并让旧事件不影响 B 的视图 |
| 回到 A 时怎么处理 | 复用一个已明确标识的缓存结果 | 启动 p3;本练习没有定义缓存策略 |
| 停止当前预览后怎么处理 | 继续接受它最终到达的完成事件 | 将其标记为不再允许更新视图,让停止动作产生稳定的可见结果 |
另一种策略本身并不是缺陷。在修改状态处理前,编程 Agent 需要知道产品究竟想采用哪一种。
把症状变成可观察的请求
一段虚构的口述可能是:
“我先点 A,再点 B,结果回来的顺序很奇怪。有时我还会回到 A。让屏幕一直保留正确的预览。”
不预设实现方式的请求可以写得更精确:
任务:让项目预览面板跟随当前请求。
把六个事件 E1–E6 作为预期行为案例。
只用所选项目作为身份是不够的:p1 和 p3 都对应 A,
但最后等待完成的视图只属于 p3。
选定行为:
- 每次选择都发起新请求;不隐式复用缓存。
- 清空上一次预览,显示“正在加载 [所选项目]”。
- 只为当前、仍允许更新视图且仍等待完成的请求装入成功或错误。
- 旧完成、旧错误或旧清理事件不能替换当前预览,
也不能改变其加载或错误状态。
- 主动停止当前请求时,显示“预览已停止”,
并使该请求之后不再允许填入视图。
- 使用实际支持的机制请求停止过时工作。
不要把停止请求描述成已确认的服务器回滚。
保留:
选择控件与预览内容格式。
不要增加撤销功能、修改服务器端数据或重新设计导航。
返回:
当前事件与归属处理路径、有针对性的改动提案,以及事件案例的检查记录。
记录实际采用的实现假设;未执行的检查与已观察行为分开保留。
检查事件顺序,而不只看一张截图
| 拟进行的检查 | 预期结果 | 实际观察 |
|---|---|---|
| 先交付 p2,再在 E5 之后交付旧 p1 | E4 显示 B;到 E6,p1 不替换 p3 等待完成的视图,也不结束其等待状态 | — |
| 将旧 p1 的成功改为旧错误后交付 | 不把它装入为当前 p3 的错误 | — |
| 在 p3 等待完成时交付旧 p1 的清理通知 | p3 仍显示加载中 | — |
| 停止当前 p3,再交付 p3 的完成事件 | 保持停止状态;不装入预览 | — |
| 未停止 p3 时交付当前 p3 的成功事件 | 显示 A 的 p3 预览,结束它的加载状态 | — |
表内结果均为人工推演的预期。产生结果的工作、取消支持、事件路由、缓存行为与任何远端副作用,都需要检查实现后才能确定。本指南没有实际运行其中任何一项。
一份有用的请求会说清每一步的可见视图属于谁。这样,实现者才能解决真正的时序问题,同时分开处理取消、显示策略与已完成动作的撤销。