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

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

一旦路由搬进浏览器、导航变成「自己改一份状态」(见 客户端路由),就多出一批服务端跳转根本没有的能力——因为导航不再是「发请求、整页换」的一次性动作,而是一个你能在中途插手的过程。本页拆三个最常用的「软」特性:导航守卫(在跳转真正发生前拦一道)、滚动恢复(后退回到原来的滚动位置)、异步导航(等数据加载好再切视图)。

1 · 导航守卫 · 在跳转「发生前」拦一道

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

守卫链的内核去掉框架细节只剩一个循环:返回 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,
];

2 · 滚动恢复 · 后退该回到你离开时的位置

整页跳转时浏览器天生会记住每页的滚动位置,后退能回到原处。但 SPA 自己换视图、DOM 是原地替换的,这套天生行为会失灵——于是 client router 得自己接管:离开一个页面时存下当前 scrollTop、绑在那条 history 条目上,后退回来时再恢复。下面这个框是真的可滚动:在 A 页滚到中间 → 去 B 页 → 点后退,看「记忆」开 / 关两种结果。

3 · 异步导航 · 等数据到位再切视图

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

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

4 · 相关页

  • 本系列 · 客户端路由——守卫拦的、异步挂起的、滚动要恢复的,都是该页那个 navigate() 动作——本页是给它加「中途钩子」。
  • 本系列 · HTTP 重定向——guard 的「改道」是客户端版重定向:不发 3xx、不往返服务端,直接在导航前把目标换成 /login
  • React Router · loader · reactrouter.com——对应异步导航一节:把「这个路由需要的数据」声明成 loader,router 在切视图前先 await 它——异步导航的工业级实现。