题目与适用场景
为一个结账表单设计验证流程。字段包括邮箱、国家、邮编、配送日期和可选优惠码。邮编规则 随国家变化,优惠码需要远程校验。价格、库存、地址规则和优惠码有效性最终都由服务端判定。
表单必须支持键盘和辅助技术,在 200% 缩放以及服务端往返后仍可用。错误不能只靠颜色表达。 提交失败后必须保留已有输入,指出全部可操作错误,并提供直接的修正路径。网络失败、优惠码 过期,以及旧异步响应晚于新响应返回,都属于题目范围。
目标不是发明表单库,而是定义状态、验证时机、语义关系、焦点策略和前后端错误契约,再说明 如何验证这些决策。原生 HTML 约束是基础;自定义行为只能增强清晰度,不能移除浏览器语义。
面试官考察什么
第一是职责边界。客户端验证用于快速反馈和减少无效请求,服务端验证决定订单是否成立。只信任 客户端的 required 或折扣金额,会留下安全与正确性漏洞。
第二是恢复能力。无效边框不够。每个错误都需要文字、与控件的程序化关联和修正说明。提交时, 用户还需要总览和可预期的焦点位置,已经正确填写的内容不能丢失。
第三是时机。用户仍在输入邮箱时立即显示“无效”,会制造噪声。第一次提交时统一验证;字段已经 失败后,再在失焦或有意义的变更时复验,让用户知道修正是否生效。异步验证还要有等待状态和 过期响应保护。
最后是验证深度。自动化无障碍扫描能发现标签缺失,却不能证明播报是否合理、焦点顺序是否正确、 服务端返回后能否恢复,以及乱序优惠码响应是否会覆盖新值。这些都需要交互测试。
回答前应澄清的问题
- 哪些校验可在本地完成? 必填、格式和简单跨字段规则可本地执行;价格、库存、资格和最终
优惠有效性属于服务端。
- 何时显示反馈? 提交时显示全部阻断错误。已经失败的字段可在失焦或明确修改后复验;不要
每次按键都播报。
- 服务端会重新渲染页面吗? 增强提交和普通表单提交都必须保留输入并渲染同一套结构化错误。
- 一个错误会不会涉及多个控件? 国家与邮编共享一条规则。共享说明放在字段组附近,汇总链接
指向开始修正的控件。
- 优惠码校验期间如何处理? 等待是信息状态,不是错误。当前值和请求代次共同决定响应是否仍
有效。
- 失败后焦点移到哪里? 多个错误时聚焦表单前的错误汇总;紧凑表单只有一个错误时,可聚焦
对应控件。应选定一种策略,避免同一变化重复播报。
- 哪些失败不是字段错误? 网络故障或库存变化属于表单级消息,并应提供重试路径,不能挂在
邮箱或邮编下。
- 流程是否会暴露敏感事实? 登录与账号恢复可能需要通用服务端消息;修正建议不能泄露账号
是否存在。
30 秒回答框架
“我先使用带标签的原生控件和约束,但最终以服务端校验为准。每个无效控件设置 aria-invalid="true",并用 aria-describedby 关联稳定的文字错误;颜色和图标只做辅助。 第一次失败提交会在表单前渲染带字段链接的错误汇总,并把焦点移到汇总,已有输入保持不变。
首次在提交时验证,失败过的字段再在失焦或有意义的修改时复验。优惠码请求会防抖、可取消,并 携带代次,旧响应不能覆盖新值。服务端以结构化格式返回已知字段错误和表单级错误。除自动化检查 外,我还会测试键盘和读屏恢复、缩放与强制颜色、服务端往返、禁用 JavaScript 的提交,以及异步 竞态。”
分步深入设计
第一步:定义状态与不变量
把字段值与验证状态分开。每个字段可处于未触碰、等待、有效或无效状态,错误码映射为本地化 文字;表单级错误单独保存。记录是否已经尝试提交,避免把输入到一半的字段直接标成失败。
不变量包括:标签始终可见;说明先于错误存在;每条可见字段错误都有程序化关联;隐藏错误不被 引用;一次提交只有一次明确焦点移动;验证失败不会清空正确输入;过期异步响应不能修改当前状态。
第二步:先建立语义控件,再补 ARIA
使用 label、最准确的输入类型、required、autocomplete 和合适的长度或模式约束。国家相关 邮编规则可以用 setCustomValidity()。值有效后必须用空字符串清除自定义错误,否则浏览器仍会 阻止提交。
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid="true"
aria-describedby="email-hint email-error">
<p id="email-hint">name@example.com</p>
<p id="email-error">Enter a valid email address.</p>checkValidity() 只检查约束,reportValidity() 还会要求浏览器展示反馈。产品若渲染自己的 无障碍错误消息,就应一致地接管无效状态,避免同时出现两套互相竞争的错误系统。
第三步:有意选择验证时机
第一次提交时验证全部字段并显示所有阻断错误。此后,无效字段在失焦时复验。对选择项或形态已经 完整且无歧义的修正值,可以在变更后更快清除错误。日期或邮箱尚未输入完整时,不要让 live region 反复播报。
跨字段规则在任一依赖变化时运行。国家变化后要重新验证邮编并更新可见说明。除非规范化安全且 可逆,否则不要悄悄改写输入;去掉首尾空格与猜测邮编格式是两种不同操作。
第四步:同时提供行内错误与可导航汇总
在字段附近渲染简短文字,仅在无效时设置 aria-invalid,并引用错误的稳定 ID。视觉样式在强制 颜色模式下也要可辨识,而且必须包含文字,不能只有红框或警告图标。
多错误提交失败后,在表单前插入带标题的汇总。为其提供临时可聚焦能力,只聚焦一次,说明错误 数量,并把每项链接到对应控件。链接文字应包含字段名和修正方式。同一事件中不要又把焦点移到 首个错误字段,否则用户很难检查汇总。
第五步:让异步校验抵抗竞态
优惠码形态完整后再防抖发起请求。值变化时取消上一个请求,完成时还要比较单调递增的请求代次。 两层保护都需要,因为取消发生时,响应可能已经进入后续处理。
通过礼貌级 live region 在字段旁显示“正在检查”。等待期间禁用应用优惠动作,但不要禁用无关 字段。超时应按产品契约成为可重试的表单级或优惠码状态,不能说成“优惠码无效”。缓存结果必须 包含使它成立的价格上下文和过期时间。
第六步:协调服务端权威错误
通过真实表单 action 或增强请求提交原始值。服务端再次规范化和校验、计算总价,并返回字段错误码、 表单错误码和接收值等结构。客户端只映射白名单字段名。未知键转为安全的表单级错误,不能直接 变成选择器或标记内容。
拒绝时保留全部非敏感输入,用服务端事实替换客户端推测,渲染同一错误汇总并聚焦。成功时显示 明确确认,并防止重复触发。如果提交后的响应丢失,应用幂等键或先查询订单,不能直接鼓励用户 再次扣款。
第七步:验证恢复路径,而不只比较快照
测试空提交、单错误、多错误、国家与邮编依赖、过期配送日期、优惠码超时、无效优惠码、快速切换 优惠码、仅服务端发现的错误,以及成功响应丢失。断言输入仍在、汇总链接聚焦正确控件、修正后错误 消失、旧异步响应被忽略。
再用纯键盘和读屏完整操作流程,检查 200% 缩放、重排、可见焦点、强制颜色、浏览器自动填充, 以及没有客户端 JavaScript 时的普通服务端提交。自动化无障碍和单元测试只能补充,不能替代播报 与焦点测试。
高质量示范回答
“我会把字段值与 touched、pending 和 error 状态分开。原生标签、输入类型、自动填充 token 和 约束提供基础。跨字段自定义规则使用 setCustomValidity(),通过时一定清空消息。客户端校验改善 反馈速度,服务端在接单前重新验证价格、库存、地址和优惠。
第一次提交会显示全部阻断错误。每个无效输入设置 aria-invalid="true",并通过 aria-describedby 引用可见修正文字。多个错误时,我在表单前渲染汇总,只聚焦一次,并把每项 链接到控件。正确值保持不变。此后失败字段在失焦或有意义的修改时复验,不会每次按键都触发播报。
优惠码校验经过防抖,同时携带取消信号和代次。只有匹配当前值的响应可以更新页面。等待、不可用 与无效是三种状态。服务端字段码经白名单映射,全局故障留在表单级。自动化检查通过后,还必须完成 键盘、读屏、缩放、无脚本服务端往返、竞态和重复提交测试。”
常见错误
- 只显示红框 → 部分用户无法感知状态 → 增加修正文字、程序化关联和非颜色提示。
- 每次按键都验证 → 未完成输入不断制造噪声 → 提交时验证,之后在合适边界复验失败字段。
- 没有清空
setCustomValidity()→ 修正后的输入仍无效 → 规则通过时设为空字符串。 - 同时聚焦汇总与首个字段 → 播报和焦点互相争抢 → 只做一次明确焦点移动,用链接导航。
- 把超时当作无效优惠码 → 基础设施故障变成错误归责 → 分别表达等待、不可用和无效。
- 接受最后返回的响应 → 过期校验覆盖当前输入 → 取消旧任务并比较请求代次。
- 信任客户端总价 → 请求可绕过界面 → 服务端重新计算并校验。
- 失败后清空表单 → 错误恢复变成重新输入 → 保留有效非敏感值,只更新错误状态。
追问与回答
追问一:错误汇总应使用 role="alert" 吗?
动态插入的错误可以通过 alert 或合适的 live region 播报,焦点也能让用户读到它。焦点与强提醒 同时使用可能重复播报,必须测试选定组合。整页服务端跳转时,把错误数量放入页面标题和主标题, 能更早提供上下文。
追问二:为什么不在表单有效前禁用提交按钮?
禁用按钮可能隐藏剩余问题,在某些导航方式下也无法到达。保留提交路径,让用户主动获得完整验证 结果。只在真实提交进行中暂时禁用,说明状态,并确保失败后可恢复。
追问三:何时应使用 aria-describedby,何时使用 live region?
aria-describedby 让用户到达字段时获得当前说明与错误;live region 在不移动焦点时播报有意义的 动态变化。静态行内错误无需全部成为 live region,否则会一次播报过多内容。批量错误使用汇总, 真正异步的状态使用礼貌播报。
追问四:如何在没有 JavaScript 时支持服务端验证?
使用真实表单 action。服务端返回同一页面,保留输入和字段错误码,在表单前渲染汇总,并把错误数 放进标题或主标题。汇总链接使用稳定控件 ID。客户端增强也消费同一错误契约,避免两种模式漂移。
追问五:如何确定性测试异步竞态?
控制两个验证 Promise。先启动请求 A,修改值后启动 B;先让 B 返回有效,再让 A 返回无效。断言 页面仍显示 B 和当前值的结果。再覆盖取消、超时、组件卸载和等待期间重新提交。