TT Lab
开始
学习 学习路径 课程

明明在找鲸鱼,却出现了猫

拒绝过期响应的搜索 hook

在 TT Lab 中继续学习

目标

亲手实现 React+TypeScript 的搜索 hook,并检查过期响应、取消、重试和异常。 在提供的 UI 中,一边看着真实的 React 界面一边练习。不用这一个实验来代替 HTML 表单的编写和长列表优化的全部内容。

为什么重要

即使响应成功了,只要与当前用户的意图不一致,也是错误的界面。取消是回收资源,而忽略结果是保证界面的正确性。 把这两项职责分开,不支持取消的异步工具也能够进行测试。Promise 的拒绝与调用时 throw,同样是不同的路径。 需要能够读懂函数、闭包、Promise、TypeScript 联合类型和基本的 React hook。

准备

首次请运行 node /opt/react-workbench/prepare.mjs /root/react-search-lab lesson。 如果已经存在,请继续使用现有副本。不要删除或覆盖。运行时不需要 npm install。 要修改的文件是 /root/react-search-lab/src/useSearch.ts。请在一并提供的 src/types.ts 中, 读取 Query(text, revision)、Search(query, signal) 和 SearchState 的五种状态契约。 不要修改 src/types.ts 和所提供的 search 的行为。步骤中的示例是最初的框架,并不是答案。

步骤

  1. 区分发送搜索之前和等待的时候——请用 lesson 模式准备 /root/react-search-lab 副本。在 src/useSearch.ts 中导出 useSearch(request, search),并返回 SearchState。空的 request.text 是 idle,不为空则是 loading 及对应的 query。清空输入后重新提交,会回到 idle。响应处理留到下一步。
  2. 为成功、空结果、失败提供不同的提示——请调用所给的 search(request.text, controller.signal)。有 1 个以上结果是 success 及原来的 items,0 个是 empty,Error 拒绝是 error 及 error.message。每种状态都要保留对应的 query,失败之后的新搜索必须能经过 loading 后成功。不要把 search 替换成自己的实现。
  3. 不让猫抢走鲸鱼的界面——即使提交了多次搜索,并让响应逆序完成,也只留下最新一次提交的结果。在最新的请求失败之后,即使过期的成功到达,也要保持最新的错误。在忽略取消的传输下也必须成立。
  4. 过期的故障提示也要丢弃——在最新的成功之后过期的失败,以及最新的失败之后更旧的失败到达时,都不能改变界面。最新请求的 Error.message 要原样保留。也要保持前一步对成功响应的保护。
  5. 清理不再需要的请求——当新的提交使 effect 发生变化时,要让前一个请求的 signal.aborted 变为 true。最新的请求在需要期间必须为 false,组件卸载时必须为 true。分别保持取消与忽略迟到的响应。
  6. 区分相同词语的新意图——Query 的 revision 不同的同一搜索,也必须是新的请求。让相同词语的两个请求逆序完成,只会留下新的结果。空提交要回到 idle,并且之前的响应不能恢复结果。
  7. 处理在收到 Promise 之前就发生的错误——即使 search 在返回 Promise 之前抛出了 Error,也不能让整个 React 界面崩溃,而要显示 error 及其 message。之后必须能通过新的搜索正常恢复。同时保持异步拒绝、取消和竞态的契约。
  8. 重复设置与清理,并重新检查全部契约——请检查在开发环境 StrictMode 的设置→清理→重新设置中,第一个请求是否被取消,即使第一个响应很晚才到,第二个结果是否仍然保留。用全部评分一并确认第 1–7 步与这一条件。在 npm run typecheck 和 npm run build 之后,请在 3000 端口的预览中亲自确认逆序的成功、失败、空结果和重试。

参考

在工作文件夹中按 npm run typecheck、npm run build、npm start 的顺序运行, 就能看到 http://127.0.0.1:3000/ 预览。请保留末尾的 /。 修改源代码后要重新构建,并刷新预览。在起始阶段,可能还不会显示响应。 手动送达是确定性的 Promise 模型,而不是 HTTP。在真实 HTTP 对比界面中,接收的是实验服务器的合成数据。 评分会检查学生 hook 的 strict 类型和真实的 React+jsdom 状态,不会修改学生的文件。 这项检查不能代替真实浏览器的布局、性能、屏幕阅读器可用性的验证。 每个步骤都会累积检查前面的步骤。适用 128KiB 以下的普通源代码文件、工作进程 12 秒的限制。 最后的回归步骤可以用之前的正确答案通过。因为它重新验证的是已有的契约,而不是新功能。 需要时请通过+时间延长会话,并在结束前另行保存修改后的源代码和观察记录。会话结束后文件不会保留。

区分发送搜索之前和等待的时候

请用 lesson 模式准备 /root/react-search-lab 副本。在 src/useSearch.ts 中导出 useSearch(request, search),并返回 SearchState。空的 request.text 是 idle,不为空则是 loading 及对应的 query。清空输入后重新提交,会回到 idle。响应处理留到下一步。

把 useState 的初始值与在 useEffect 中处理已提交搜索的工作分开。不要直接读取搜索框中正在输入的 draft。

为成功、空结果、失败提供不同的提示

请调用所给的 search(request.text, controller.signal)。有 1 个以上结果是 success 及原来的 items,0 个是 empty,Error 拒绝是 error 及 error.message。每种状态都要保留对应的 query,失败之后的新搜索必须能经过 loading 后成功。不要把 search 替换成自己的实现。

用 AbortController 创建 signal,并连接 Promise 的 then 和 catch。empty 不是故障,而是正常响应的一种。

不让猫抢走鲸鱼的界面

即使提交了多次搜索,并让响应逆序完成,也只留下最新一次提交的结果。在最新的请求失败之后,即使过期的成功到达,也要保持最新的错误。在忽略取消的传输下也必须成立。

让成功回调检查在每次 effect 设置内部创建的有效性值,并在清理时只让那个值过期。所有请求共享的一个 true/false,可能会被重新打开。

过期的故障提示也要丢弃

在最新的成功之后过期的失败,以及最新的失败之后更旧的失败到达时,都不能改变界面。最新请求的 Error.message 要原样保留。也要保持前一步对成功响应的保护。

如果只保护 then,catch 仍然可以改变界面。请确认两条路径读取的是同一个 effect 的有效期。

清理不再需要的请求

当新的提交使 effect 发生变化时,要让前一个请求的 signal.aborted 变为 true。最新的请求在需要期间必须为 false,组件卸载时必须为 true。分别保持取消与忽略迟到的响应。

在清理函数中,只取消这个 effect 创建的 controller。仅仅忽略结果,并不能让外部工作停下来。

区分相同词语的新意图

Query 的 revision 不同的同一搜索,也必须是新的请求。让相同词语的两个请求逆序完成,只会留下新的结果。空提交要回到 idle,并且之前的响应不能恢复结果。

如果依赖中只有搜索字符串,就会漏掉相同词语的重新提交。请利用所提供 UI 的契约:每次提交,request 都是新对象。

处理在收到 Promise 之前就发生的错误

即使 search 在返回 Promise 之前抛出了 Error,也不能让整个 React 界面崩溃,而要显示 error 及其 message。之后必须能通过新的搜索正常恢复。同时保持异步拒绝、取消和竞态的契约。

在 search(...).catch(...) 中,search 本身的 throw 到不了 catch。请把调用移到 Promise 回调内部,或者用 try/catch 同时处理两条路径。

重复设置与清理,并重新检查全部契约

请检查在开发环境 StrictMode 的设置→清理→重新设置中,第一个请求是否被取消,即使第一个响应很晚才到,第二个结果是否仍然保留。用全部评分一并确认第 1–7 步与这一条件。在 npm run typecheck 和 npm run build 之后,请在 3000 端口的预览中亲自确认逆序的成功、失败、空结果和重试。

最后一步不是添加新标志的任务,而是对累积实现的回归检查。不要假定开发环境 StrictMode 的调用次数在生产环境中也相同。