代表性面试主题

前端面试:如何用 Navigation API 设计可恢复的 SPA 导航?

前端困难
Offer.cc 编辑团队发布 更新

题干

如果要把一个 SPA 的导航改造成可恢复、可观测且不破坏浏览器历史的实现,你会如何使用 Navigation API?请说明加载竞态、错误回退、滚动恢复和不支持该 API 时的方案。

题干与适用场景

如果要把一个 SPA 的导航改造成可恢复、可观测且不破坏浏览器历史的实现,你会如何使用 Navigation API?请说明加载竞态、错误回退、滚动恢复和不支持该 API 时的方案。

这道题适合前端、全栈和 Web 平台岗位。它考察的是浏览器导航模型、异步状态机和渐进增强,而不是记住某个框架的路由配置。Navigation API 用一个统一入口观察同一文档内的导航,并提供 navigate 事件、intercept()navigatesuccessnavigateerror 等能力;它不能替代首屏服务端渲染,也不能改变跨文档导航的安全边界。

面试官考察点

  • 是否把 URL 和历史记录作为导航状态的来源,而不是只依赖组件内部状态。
  • 是否区分同文档路由、跨文档链接、下载、表单提交和跨域跳转。
  • 是否处理用户连续点击造成的过期请求、取消、错误和回退。
  • 是否说明滚动位置、页面状态和浏览器前进后退的恢复策略。
  • 是否保留首屏渲染和不支持 Navigation API 浏览器的降级路径。
  • 是否能用可观测事件定义成功、失败、超时和回滚,而不是只说“切换组件”。

30 秒回答框架

“我先保留服务端首屏和普通链接,把 URL 与历史记录作为唯一导航事实来源。支持 Navigation API 时,只拦截同源、同文档且属于应用路由的导航,在 navigate 事件中启动带取消信号的加载;新导航到来就取消旧任务,只有当前任务能提交界面。成功后恢复滚动和页面状态,失败则显示可重试状态并保留原页面。浏览器不支持时继续使用现有 History API 路径,所有路径共享同一加载器和监控指标。”

分步骤深入解答

第一步:划定可以拦截的导航

先检查目标 URL 是否同源、是否属于应用路由、是否需要下载或完整文档响应。外部链接、文件下载、跨域跳转、特殊协议和表单语义不应被 SPA 接管。首屏请求仍由服务器和浏览器完成,navigate 事件不能弥补刷新后没有应用运行时的问题。

navigate 事件中只对安全的应用路由调用 intercept()。拦截处理器负责获取数据、更新视图和恢复滚动;不满足条件时让浏览器执行默认导航。这样既保留可访问性,也避免把平台导航强行变成客户端状态。

第二步:把导航实现成可取消的状态机

每次导航生成递增序号,并把 AbortSignal 传给数据加载器。用户从 /search?q=a 很快点到 /search?q=ab 时,前一个请求即使晚返回,也不能覆盖后一个结果。提交前同时检查信号未取消、序号仍是最新值、目标 URL 与当前状态一致。

js
let latestNavigation = 0;

navigation.addEventListener("navigate", (event) => {
  if (!event.canIntercept || !isAppRoute(event.destination.url)) return;
  const id = ++latestNavigation;
  event.intercept({
    async handler() {
      const data = await loadRoute(event.destination.url, event.signal);
      if (event.signal.aborted || id !== latestNavigation) return;
      renderRoute(data);
    }
  });
});

示例只表达竞态原则。真实实现还要处理加载器抛错、超时、缓存命中和组件卸载。不要用“最后返回的请求获胜”,因为网络完成顺序不代表用户最后一次意图。

第三步:提交历史、滚动和页面状态

导航成功后,以目标 URL 作为提交结果。需要把可恢复的小型状态写入当前历史条目时,可以使用 updateCurrentEntry();大对象和敏感数据不应塞进历史状态。滚动策略要区分新页面、前进后退和锚点跳转:新路由通常滚到顶部,返回历史条目则恢复保存的位置。

不要在加载开始时就修改标题、选中项和面包屑,除非这些变化有明确的待处理状态。成功事件后再提交主要视觉状态,失败时保留原内容并给出重试入口。navigatesuccessnavigateerror 可以作为统一的耗时、取消率和失败率观测点。

第四步:处理错误、回退和恢复

加载失败时,原页面仍应可用;错误区域要包含重试、返回和重新加载等下一步动作。对权限失效、资源不存在和网络暂时不可用分别处理,不要把所有异常改写成 404。若目标页面需要完整文档响应,则放弃拦截,让浏览器走默认导航。

当应用状态损坏或客户端脚本加载失败,普通链接和服务器路由仍应能打开页面。路由数据要可从 URL 重建,不能只依赖内存缓存。恢复流程应记录目标 URL、导航序号、错误类型和是否发生回退,便于定位“用户看到旧页面但地址已改变”的问题。

第五步:设计兼容性和渐进增强

Navigation API 在较新的浏览器中可用,但产品不能把它当成唯一入口。先检测 window.navigation 和所需事件、方法,再选择增强路径;不支持时复用同一个路由加载器,通过现有 History API 和框架机制完成导航。不要为了兼容性复制两套业务规则。

兼容层要覆盖首屏、刷新、前进后退、链接中断和异常回退。测试矩阵至少包含支持与不支持 API 的浏览器、慢网、快速连续导航、跨域链接、下载链接和脚本加载失败。能力检测应与功能使用点接近,避免版本判断造成错误分支。

第六步:验证性能、可访问性和可观测性

以真实用户任务验证:搜索、筛选、返回、刷新、深链打开和错误重试。记录导航开始到可交互的时间、取消率、失败率、回退率和重复请求;把这些指标按浏览器能力分组。不要只测组件渲染耗时,因为导航失败可能来自数据、权限或历史状态。

保留原生链接语义、键盘操作和辅助技术可感知的焦点移动。拦截后应更新文档标题,并把焦点放到页面主内容;不应阻止用户在新标签打开链接。性能优化要以可恢复为前提,缓存只能减少延迟,不能成为恢复所需的唯一数据源。

信息增益与边界

Navigation API 的价值在于把平台导航事件、历史记录和可取消加载放到同一条状态链上。它不能保证数据源可用,也不能让跨域页面变成同文档路由。面试中应明确:拦截范围、提交时机、失败时的旧页面策略,以及浏览器能力不足时的默认行为。

高质量示范回答

“我会先把导航分成同源应用路由、跨文档页面、下载、表单和跨域跳转,只拦截前一类。首屏和普通链接继续可用,URL 与历史记录作为事实来源。支持 Navigation API 时,在 navigate 事件中调用 intercept(),为每次导航建立序号和取消信号;新导航会取消旧加载,提交结果前再次确认它仍是最新任务。

加载成功后更新视图、标题、焦点和滚动策略。新路由滚到顶部,前进后退恢复历史位置;需要保存的小状态用当前历史条目,避免把大对象或敏感信息写进去。加载失败时保留旧页面,显示可重试、返回或完整刷新动作,并通过 navigatesuccessnavigateerror 和服务端日志记录耗时、取消和错误类型。

如果浏览器不支持 Navigation API,就让同一个加载器走 History API 或框架路由;外部链接和下载始终使用默认浏览器行为。最后用深链、刷新、连续点击、慢网、跨域链接、脚本失败和辅助技术任务验证,确认地址、内容、焦点和历史记录不会互相失步。这样得到的是渐进增强的导航层,而不是只在新浏览器里工作的客户端替换。”

常见错误

  • 拦截所有链接 → 下载、跨域和表单语义会被破坏 → 只拦截可确认的同源应用路由。
  • 让最后返回的请求获胜 → 旧请求可能覆盖用户最新意图 → 使用取消信号和导航序号。
  • 只测组件渲染 → 数据、权限和历史状态错误被遗漏 → 覆盖完整导航任务和失败回退。
  • 把 Navigation API 当成首屏方案 → 刷新或脚本失败时页面无法恢复 → 保留服务器响应和普通链接。
  • 把状态全部放进历史条目 → 数据过大或泄露敏感信息 → 只保存可重建的小状态。
  • 兼容层复制一套业务逻辑 → 两条路径会逐渐产生不同错误 → 共享加载器、状态机和监控。

追问及应对

用户连续点击两个目标,旧请求已经写入缓存怎么办?

缓存写入可以允许,但界面提交必须检查取消信号和最新序号。缓存条目带上 URL、参数和版本,后续请求只能读取与当前目标匹配的数据;不能让过期结果直接改变页面。

目标页面权限失效,应该回退还是跳登录?

先依据服务端明确的状态码区分未登录、无权限和资源不存在。未登录可转到登录流程并保存返回 URL;无权限保留当前页面的解释和返回动作。不要把权限错误伪装成普通网络失败。

如何测试浏览器不支持 Navigation API 的路径?

在能力检测分支下运行同一组深链、刷新、前进后退、慢网和错误用例,并检查地址、内容、标题、焦点和滚动。测试目标是行为一致,不是要求两个实现的内部事件完全相同。

为什么不直接完全依赖框架路由?

框架路由可以复用,但题目要考察浏览器导航边界。需要明确哪些导航必须由浏览器处理、哪些可以增强,以及框架事件如何映射到取消、历史和错误语义。回答应以平台行为为基线,再说明框架适配。

公开来源

同类题目