软导航 · 守卫、滚动恢复与异步
路由搬进浏览器、导航变成「自己改一份状态」之后(见客户端路由),就多出一批服务端跳转没有的能力——导航不再是「发请求、整页换」的一次性动作,而是一个可以中途插手的过程。本页拆三个最常用的软特性。
1 · 导航守卫
每次 navigate(to) 真正改栈之前,先依次跑一串 guard。每个 guard 拿到 (to, from),可以放行、取消(留在原地)、或改道到别处。第一个取消或改道的 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,
];
guard 的改道是客户端版的重定向:不发 3xx、不往返服务端,直接在导航前把目标换成 /login。
2 · 滚动恢复
整页跳转时浏览器天生会记住每页的滚动位置,后退能回到原处。但 SPA 自己换视图、DOM 是原地替换的,这套天生行为会失灵,于是 client router 得自己接管:离开一个页面时存下当前 scrollTop、绑在那条 history 条目上,后退回来时再恢复。
3 · 异步导航
点一个链接,目标视图常常要先取一段数据才能渲染。整页跳转期间浏览器只能白屏等着。client router 不必:它可以先把导航挂起,留在旧视图或显示加载态,等数据 await 回来再一次性提交切过去。这就是 React Router 的 loader 与导航期间的 pending
状态,还能在慢请求时给一条进度反馈而不是一片空白。
三件事的共同点是导航成了过程而非瞬间。服务端跳转里这些都不存在——一个 3xx 发出去就结束了,无从拦截、无从挂起、更不记滚动。把路由搬进客户端、自己持有 location
状态之后,导航的每个阶段都成了可插手的钩子:发生前拦(守卫)、过程中等(异步)、完成后复位(滚动)。代价是这些过去浏览器代劳的事,现在得自己正确实现。