系统设计 / 路由设计 · 稳定入口与可变目标 / 软导航 · 守卫、滚动恢复与异步 待审核 10 / 23

软导航 · 守卫、滚动恢复与异步

路由搬进浏览器、导航变成「自己改一份状态」之后(见客户端路由),就多出一批服务端跳转没有的能力——导航不再是「发请求、整页换」的一次性动作,而是一个可以中途插手的过程。本页拆三个最常用的软特性。

1 · 导航守卫

每次 navigate(to) 真正改栈之前,先依次跑一串 guard。每个 guard 拿到 (to, from),可以放行、取消(留在原地)、或改道到别处。第一个取消或改道的 guard 就截断整条链,后面的不再跑。最典型的两种:未登录访问 /admin 改道 /login;有未保存的表单想离开则取消并弹确认。

图 1-1 · 守卫链的逐个执行与短路。可切换登录态与表单脏标记,观察哪个 guard 先表态、整条链在哪里被截断。

守卫链的内核去掉框架细节只剩一个循环:返回 false 即取消导航、返回字符串即改道、其余放行,第一个表态的 guard 就终止整条链。

function navigate(to, from) {
  for (const guard of beforeEach) {
    const r = guard(to, from);
    if (r === false) return;
    if (typeof r === 'string') return navigate(r, from);
  }
  commit(to);
}

beforeEach = [
  (to)       => to.startsWith('/admin') && !loggedIn ? '/login' : true,
  (to, from) => from.startsWith('/edit') && dirty    ? false    : true,
];

guard 的改道是客户端版的重定向:不发 3xx、不往返服务端,直接在导航前把目标换成 /login

2 · 滚动恢复

整页跳转时浏览器天生会记住每页的滚动位置,后退能回到原处。但 SPA 自己换视图、DOM 是原地替换的,这套天生行为会失灵,于是 client router 得自己接管:离开一个页面时存下当前 scrollTop、绑在那条 history 条目上,后退回来时再恢复。

图 2-1 · 一个真的可滚动的框。可在 A 页滚到中间、去 B 页、再点后退,对比「记忆」开与关两种结果。

3 · 异步导航

点一个链接,目标视图常常要先取一段数据才能渲染。整页跳转期间浏览器只能白屏等着。client router 不必:它可以先把导航挂起,留在旧视图或显示加载态,等数据 await 回来再一次性提交切过去。这就是 React Router 的 loader 与导航期间的 pending 状态,还能在慢请求时给一条进度反馈而不是一片空白。

图 3-1 · 导航被挂起、数据到位后再提交的过程。可调延迟观察 pending 阶段停在旧视图而非白屏。

三件事的共同点是导航成了过程而非瞬间。服务端跳转里这些都不存在——一个 3xx 发出去就结束了,无从拦截、无从挂起、更不记滚动。把路由搬进客户端、自己持有 location 状态之后,导航的每个阶段都成了可插手的钩子:发生前拦(守卫)、过程中等(异步)、完成后复位(滚动)。代价是这些过去浏览器代劳的事,现在得自己正确实现。