代表性面试主题

前端面试:SPA 路由切换后如何管理焦点?

前端困难
OfferCC 编辑团队发布 更新

题干

一个客户端渲染的电商 SPA 会保留页头和侧栏,只替换主内容。跳转后,键盘和屏幕阅读器用户可能仍停在旧触发控件上,或失去新页面的上下文。路由会异步加载,导航可能被替换,筛选条件会修改 URL 但不开始新任务,后退与前进还应恢复有用的上下文。请设计焦点、播报、竞态处理、历史恢复和测试策略。

问题与使用场景

完整页面跳转会替换文档及其标题。SPA 软跳转继续使用同一文档,焦点可能留在一个周围内容已经消失的链接上。屏幕阅读器用户可能不知道新的商品页、结账步骤或错误页已经就绪。

策略需要区分五类变化:首次加载、用户发起的新路由、当前任务内的筛选或排序、被替换或取消的导航,以及浏览器后退/前进。它还要与弹窗等组件自己的焦点协议共存。目标是提供可预测的上下文,而非每次 URL 变化都移动焦点。

面试官考察什么

面试官首先看候选人能否建立语义决策模型。“每次路由变化都聚焦标题”看似无障碍,却会破坏筛选、锚点变化、首次注水和历史恢复。强回答先判断用户的任务是否改变。

其次是浏览器基础:tabIndex={-1} 可让标题接收程序化焦点,又不会把它加入顺序 Tab 路径;focus() 默认可能滚动元素;preventScroll 能拆分焦点恢复和滚动恢复;正数 tabindex 会制造脆弱的焦点顺序。

最后是并发处理。如果路由 B 比 C 更晚完成,B 不能改回标题或抢走焦点。自动化能验证活动元素、标题和顺序,却不能证明每一种浏览器与辅助技术组合会如何播报。

回答前的澄清问题

  • 哪些变化会开始新任务? 从商品页进入结账是新任务,调整排序通常不是。这决定焦点移动还是留在触发控件。
  • 何时算路由提交完成? 只在胜出的路由已经渲染最终主标题后聚焦,不在点击、URL 变化或骨架屏出现时执行。
  • 浮层由谁管理? 弹窗负责自己的初始焦点、焦点约束和关闭后恢复;路由策略不能与它竞争。
  • 历史记录要恢复什么? 明确每个历史条目恢复焦点、滚动位置或二者。两者必须协调,避免连续跳动。
  • 支持哪些浏览器和辅助技术? 播报行为存在差异,发布测试矩阵必须明确。
  • 每个路由能否提供描述性标题和唯一主标题? 把它们设为路由契约;main 地标只作受控降级。

30 秒回答框架

“我先对导航分类,再决定是否移动焦点。首次加载不强制聚焦。用户发起的新任务路由只有在最终提交后才更新文档标题,并聚焦设置了 tabIndex=-1 的描述性 H1。同一任务内的筛选保留触发控件,通过持久的礼貌状态区播报完成结果。每次导航都有令牌,只有最新且已提交的路由能修改标题和焦点。后退/前进优先恢复该历史条目保存的有效语义目标,否则回退到 H1,并与滚动恢复协调。测试覆盖首次加载、快速连续跳转、错误、筛选、历史、键盘顺序、可见焦点及真实屏幕阅读器。”

分步深入分析

先建立转换决策表:

转换焦点动作播报方式
首次加载或注水不强制移动使用原生文档与标题行为
PUSH 到新任务聚焦已提交路由的 H1更新标题并聚焦标题
同任务筛选、排序、分页保留触发控件必要时礼貌播报结果或状态
后退/前进 POP恢复有效目标,否则 H1恢复上下文或聚焦标题
重定向或错误路由聚焦该路由最终 H1使用最终标题和主标题
弹窗打开或关闭交给弹窗协议弹窗名称与返回目标

路由契约提供最终文档标题、描述性 H1 引用和语义路由键。H1 使用 tabIndex={-1},可被代码聚焦但不增加一个 Tab 停靠点,并保留清晰的焦点样式。跳过导航链接仍应是首个可聚焦控件并指向 main;它解决重复导航绕过问题,与路由焦点并不冲突。

焦点动作属于提交边界。为每次导航尝试分配递增令牌。数据和界面完成后,仅当令牌仍为当前值且 H1 已挂载时才执行副作用。不要依赖固定延时,网络和渲染速度会让它产生竞态。

text
beginNavigation(kind):
  token = nextToken()
  rememberCurrentFocus(historyEntryKey)

commitRoute(token, kind, title, heading):
  if token != currentToken or heading is not connected:
    return
  document.title = title
  if kind is PUSH or semantic-task REPLACE:
    heading.focus()
  else if kind is POP:
    focus(validSavedTarget(historyEntryKey) or heading)

保存稳定的路由内焦点 ID,不保存自动生成的 CSS 路径。POP 时只恢复仍存在、可见、可用且在恢复状态中有意义的元素,否则聚焦 H1。若路由器独立恢复滚动位置,可使用 focus({ preventScroll: true }) 后只恢复一次滚动;普通 PUSH 通常让聚焦自然显示 H1 更清楚。

避免重复播报。先更新标题再聚焦新 H1,通常已经提供足够上下文,但具体语音取决于辅助技术。默认不要再用实时区域重复同一标题。筛选操作保留焦点时,可用持久的 role="status" 或 aria-live="polite" 播报最终结果数。assertive 可能中断当前语音,只用于真正紧急的信息。

测试判定应直接来自策略:首次注水不能抢焦点;PUSH 最终提交后,document.activeElement 是新 H1、标题已更新,下一次 Tab 到达第一个合乎逻辑的交互控件;筛选后触发控件仍有焦点,状态只更新一次;A→B→C 竞态中即使 B 最后完成,C 仍拥有标题和焦点;错误与重定向聚焦各自标题;POP 恢复有效目标或确定性降级。

自动化覆盖上述 DOM 断言、目标卸载、缩放及减少动态效果布局。随后用键盘和支持矩阵中的真实组合手测,例如 VoiceOver/Safari,以及 NVDA 或 JAWS 与支持的浏览器。核对焦点可见、顺序合理、滚动不突兀、播报可理解,并记录版本,因为语音结果是集成行为,不是单靠 DOM 就能保证的结果。

高质量示例回答

“我会把焦点行为纳入路由的语义契约。每个路由提供最终文档标题和描述性 H1 引用。首次注水不移动焦点;用户发起且改变任务的 PUSH 在最终提交后聚焦 H1;筛选和排序保留触发控件;POP 优先恢复稳定的历史目标,失败时回退到 H1。

H1 使用 tabIndex=-1,因此能被程序化聚焦而不会增加 Tab 停靠点。我保留可见焦点样式和指向 main 的跳过导航链接。标题和焦点只在胜出的路由提交后一起更新。每次导航有一个令牌,过期异步结果会被忽略,取消的路由就无法抢焦点。

历史条目保存路由内语义焦点 ID,只恢复已连接、可见且可用的目标。若滚动单独恢复,则用 preventScroll 聚焦后只处理一次滚动。同任务异步更新使用持久的礼貌状态区;不会在标题和实时区域重复播报路由名称。

自动化验证活动元素、标题、Tab 顺序、筛选保留、A→B→C 快速导航、重定向、错误和 POP 降级。最后用支持的键盘与屏幕阅读器矩阵验证真实语音、焦点样式和滚动。这样能区分确定的 DOM 行为与必须实际观察的辅助技术行为。”

常见错误

  • 每次 URL 变化都聚焦 → 筛选和锚点变化打断当前任务 → 先按语义分类。
  • 点击链接时立即聚焦 → 目标可能尚不存在或被取消 → 只处理胜出的已提交路由。
  • 使用固定延时 → 不同渲染速度产生竞态 → 使用路由生命周期和导航令牌。
  • 聚焦 body 或使用正数 tabindex → 上下文和顺序不明确 → 聚焦设置 tabIndex=-1 的描述性 H1。
  • POP 总是跳到 H1 → 后退会丢失用户位置 → 恢复有效历史目标并提供确定性降级。
  • 重复播报标题 → 用户听到冗余语音 → 路由优先用聚焦标题,同页更新才用状态区。
  • 把自动扫描当成验收 → 扫描无法验证真实语音 → 加入键盘和辅助技术手测。

追问与回答

追问 1:为什么不总是聚焦 main 地标?

H1 通常能更具体地命名新任务。路由无法提供标题时,main 可作为降级,但要求每个路由提供主标题能同时改善可见结构、文档层级和可测试性。

追问 2:鼠标点击导航后也要移动焦点吗?

用户操作替换主任务时,一致的上下文对不同输入方式都有价值,包括会点击的屏幕阅读器用户。应根据语义导航决策,不要猜测用户是否只用键盘。后台刷新不能移动焦点。

追问 3:快速跳转时 B 比 C 更晚完成怎么办?

B 的令牌已不是当前值,其提交副作用直接返回,不能修改标题、焦点或状态。中止 B 的请求可以节省资源,但取消可能太晚或不受支持,因此仍需令牌检查。

追问 4:焦点恢复和滚动恢复如何配合?

普通 focus() 可能把目标滚动到视口。POP 若由路由器恢复保存的滚动位置,就用 preventScroll 验证并聚焦目标,再执行一次滚动恢复。只能有一个明确的滚动所有者。

追问 5:什么时候使用 assertive 实时区域?

仅在延迟听到会造成严重问题时使用,例如紧急会话或安全消息。结果数量和完成状态使用 polite。路由上下文通常由最终标题和聚焦 H1 提供,避免第二次播报。

追问 6:做到这些就符合 WCAG 吗?

不能单凭这一项下结论。该策略有助于动态应用维持可理解的焦点顺序和上下文,完整符合性还取决于语义、名称、键盘操作、对比度、错误处理等要求,应按产品目标检查完整用户旅程。

公开来源

同类题目