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

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

如何把一万头鲸鱼带到屏幕上

在 TT Lab 中继续学习

目标

用 React+TypeScript 亲手编写搜索表单、状态提示,以及每页 50 条的列表。 检查界面更新时输入和焦点是否保持不变,并在浏览器中亲自操作。

为什么重要

即使收到了正确的响应,如果输入框没有名称,或者无法用键盘提交,这个界面也无法使用。 错误和空结果的提示,能帮助用户选择下一步行动。大量数据会分别产生网络接收成本和 DOM 渲染成本。 用 CSS 隐藏所有数据,与真正只绘制一部分,是不同的。需要能够读懂 JSX、props、useState、条件渲染和 TypeScript 联合类型。

准备

首次请运行 node /opt/react-workbench/prepare.mjs /root/react-ui-lab ui。 如果已有目录,请继续使用当前的工作,不要覆盖。不需要 npm install。 要修改的文件是 /root/react-ui-lab/src/SearchView.tsx,类型材料是 /root/react-ui-lab/src/types.ts。 SearchView 是以 state(SearchState)、revision(number)、onSearch(text) 作为 props 的 named export。 SearchState 的 success items 至少有 1 个,0 条是 empty。revision 在每次注入新结果时都会改变。 右侧的状态注入装置无需 HTTP 就能提供合成数据。即使不重新编写 hook 的答案,也能练习 UI。

步骤

  1. 给搜索框还原名称——请用 ui 模式准备 /root/react-ui-lab,并编写 src/SearchView.tsx。保持 SearchView named export 和所提供的 props。创建 role 为 search 的 form、与名为“搜索词”的 label 关联的最多 80 个字符的 input、名为“搜索”的 submit button。输入值由组件管理。这一步还不连接提交处理。
  2. Enter 也要发出同样的搜索——阻止 form 提交默认的刷新,并把去掉首尾空格的输入值传给 onSearch(text)。即使连续提交相同的词语,也每次都调用,如果只输入了空格,就传递空字符串。保持正在输入的文字不变,不要只依赖按钮点击。
  3. 区分空书库与故障的书库——在始终存在的状态提示区域中,设置 role=status、aria-live=polite、aria-atomic=true。用互不相同的韩语句子分别说明 idle/loading/empty/error/success。等待、空结果、成功要显示对应的 query,失败要显示传入的 message。如果不是正常状态,就不要留下之前的结果列表。
  4. 把数据显示为文字,而不是代码——请按原来的顺序显示 success 的每个 items 的 title 和 detail。创建名称为“搜索结果列表”的 ul,并用 tabIndex=0 让列表也能获得焦点。即使标题中出现了看起来像 HTML 的字符串,也不要作为标签执行,而要原样显示为文字。这一步也可以显示整个列表。
  5. 每次遇到 50 头,共 1 万头鲸鱼——请在保留原来 items 的同时,一次最多只在 DOM 中绘制 50 个条目。用名为“上一页”“下一页”的基本 button 改变显示范围。在 1 万条时,下一页从第 51 个数据开始,再点击上一页,必须回到第一个数据。只改变数字或 CSS 显示、却把全部内容留在 DOM 中的方式不算。
  6. 不会掉到最后一页之外——请把页面操作包在名称为“结果页”的 nav 中。第一页的“上一页”和最后一页的“下一页”必须是 disabled。51 条时最后一页有 1 条,恰好 50 条时只有一页。请利用这一契约:success.items 不为空,0 条结果作为 empty 传递。
  7. 不要给新的搜索带上旧的页码——revision 改变时,请从第一页开始显示结果。相同 query 的重新搜索也是新的 revision。即使从第三页变成只有 3 条的新结果,出现的也必须是第一个数据,而不是空白界面。把正在输入的文字和搜索表单的生命周期,与结果列表分开。
  8. 即使界面更新,也要保住手指所在的位置——请检查在输入过程中,即使新的结果或失败到来,输入框的 DOM、正在编写的文字和焦点是否保持不变。状态提示的 DOM 也要保持,只更新文案。重新检查前面步骤的全部契约。构建之后,请在预览中亲自确认 Tab、Enter、翻页以及 390px 宽的界面。这是回归步骤,所以可以用第 7 步的正确实现通过。

参考

执行 cd /root/react-ui-lab 之后,按 npm run typecheck、npm run build、npm start 的顺序运行。 请保留预览地址 http://127.0.0.1:3000/ 末尾的 /。修改源代码后重新构建并刷新。 第 1 步还没有 form 提交处理,所以 Enter 可能会刷新。在第 2 步中连接。 可以用 node /opt/react-workbench/grading/check-step.mjs /root/react-ui-lab/src/SearchView.tsx 8 ui 检查全部累积契约。 每个步骤都会一并检查前面的步骤。普通源代码限制为 128KiB,工作进程限制为 12 秒,评分不会修改学生文件。 本实验的 DOM 检查并不是对 CSS 可见性、颜色对比度、真实屏幕阅读器朗读的认证。 即使每次只显示 50 条,收到的数组仍会保留在内存中。请把 HTTP 时间、React Profiler 的 actualDuration、布局、绘制分开观察。 需要时请通过+时间延长,并在结束前另行保存源代码。实验会话结束后,文件不会保留。

给搜索框还原名称

请用 ui 模式准备 /root/react-ui-lab,并编写 src/SearchView.tsx。保持 SearchView named export 和所提供的 props。创建 role 为 search 的 form、与名为“搜索词”的 label 关联的最多 80 个字符的 input、名为“搜索”的 submit button。输入值由组件管理。这一步还不连接提交处理。

把 label 写在界面上,与把它关联到输入框,是不同的。请使用 htmlFor/id 配对,或者把 input 放进 label 里面。不要只用 placeholder 来代替。

Enter 也要发出同样的搜索

阻止 form 提交默认的刷新,并把去掉首尾空格的输入值传给 onSearch(text)。即使连续提交相同的词语,也每次都调用,如果只输入了空格,就传递空字符串。保持正在输入的文字不变,不要只依赖按钮点击。

搜索意图是在 form 提交时产生的,而不是在输入事件时。如果在 onSubmit 中处理,默认的 submit 按钮和 Enter 就会使用同一条路径。

区分空书库与故障的书库

在始终存在的状态提示区域中,设置 role=status、aria-live=polite、aria-atomic=true。用互不相同的韩语句子分别说明 idle/loading/empty/error/success。等待、空结果、成功要显示对应的 query,失败要显示传入的 message。如果不是正常状态,就不要留下之前的结果列表。

empty 表示搜索成功之后没有数据,error 表示请求失败了。如果合并成同样的提示,用户就无法决定是该修改搜索词还是重试。

把数据显示为文字,而不是代码

请按原来的顺序显示 success 的每个 items 的 title 和 detail。创建名称为“搜索结果列表”的 ul,并用 tabIndex=0 让列表也能获得焦点。即使标题中出现了看起来像 HTML 的字符串,也不要作为标签执行,而要原样显示为文字。这一步也可以显示整个列表。

把值放在 JSX 的文本位置上,数据就不会被解释为 HTML。列表的 key 请使用数据稳定的 id,也不要漏掉说明。

每次遇到 50 头,共 1 万头鲸鱼

请在保留原来 items 的同时,一次最多只在 DOM 中绘制 50 个条目。用名为“上一页”“下一页”的基本 button 改变显示范围。在 1 万条时,下一页从第 51 个数据开始,再点击上一页,必须回到第一个数据。只改变数字或 CSS 显示、却把全部内容留在 DOM 中的方式不算。

请根据当前页码计算 slice 的起点和终点。要区分的是:接收到的数据量不变,减少的只是要渲染的行数。

不会掉到最后一页之外

请把页面操作包在名称为“结果页”的 nav 中。第一页的“上一页”和最后一页的“下一页”必须是 disabled。51 条时最后一页有 1 条,恰好 50 条时只有一页。请利用这一契约:success.items 不为空,0 条结果作为 empty 传递。

除法向上取整,就能得到所需的页数。最后一个索引与页数相差 1。不要只把颜色变淡,而要使用真正的 disabled 属性。

不要给新的搜索带上旧的页码

revision 改变时,请从第一页开始显示结果。相同 query 的重新搜索也是新的 revision。即使从第三页变成只有 3 条的新结果,出现的也必须是第一个数据,而不是空白界面。把正在输入的文字和搜索表单的生命周期,与结果列表分开。

只要用新的 revision 重新创建用于结果的子组件,就能重置页面状态。如果连表单也用 key 重新创建,输入和焦点也会一起消失。

即使界面更新,也要保住手指所在的位置

请检查在输入过程中,即使新的结果或失败到来,输入框的 DOM、正在编写的文字和焦点是否保持不变。状态提示的 DOM 也要保持,只更新文案。重新检查前面步骤的全部契约。构建之后,请在预览中亲自确认 Tab、Enter、翻页以及 390px 宽的界面。这是回归步骤,所以可以用第 7 步的正确实现通过。

不要强行把焦点移到结果上。DOM 自动检查与真实浏览器的键盘和布局观察是不同的证据,并不能认证屏幕阅读器的可用性。