Notes

路由与 URL State:统一心智模型(架构师视角)

一句话结论:在中大型前端应用里,路由不只是“页面跳转”,它本质上是全局状态机 + 可回放的状态容器——其中 URL(path + search + hash)是最可靠的“可分享/可回退/可重放”的状态载体;当你把“哪些状态应该进 URL”这件事定清楚,路由设计、数据预取、缓存命中、错误/加载边界、以及导航性能都会变得可控。

1. 为什么路由是架构问题,而不是框架 API 问题?

项目变复杂后,路由通常承载:

  • 信息架构:页面/模块如何组织(目录结构、导航结构、权限结构)
  • 状态承载:筛选/分页/排序/选中项/tab 是否需要回放
  • 数据获取:进入页面前是否要预取、如何避免瀑布
  • 体验治理:切页是否闪屏、是否保留旧 UI、骨架屏如何分层
  • 可靠性:404/500/鉴权失败/局部模块错误怎么处理

所以“路由怎么做”必须先回答 3 个架构问题:

  1. URL 是不是 source of truth?哪些状态要进 URL?
  2. 路由进入与数据获取的边界在哪里?(loader / server fetch / client fetch)
  3. 导航时 UI 体验策略是什么?(prefetch / skeleton / transition / suspense)

2. URL State 是什么?为什么它最重要?

URL State 指:为了满足以下任一需求,必须被编码进 URL 的状态:

  • 可分享:复制链接给同事/用户,打开后视图一致
  • 可回退:浏览器 back/forward 后视图一致
  • 可重放:刷新页面后视图一致(至少恢复到同一查询条件/同一上下文)

典型 URL state:

  • 搜索关键字、筛选条件、分页、排序
  • 当前选中的 itemId(列表-详情联动)
  • tab(如果它代表用户可分享的视图语义)

经验:只要它影响“用户看到的业务视图”,且用户会期望能通过链接还原,就应该考虑进 URL。

3. 机制:把路由当成“状态机”来设计

你可以用状态机视角理解路由:

  • state:当前 route(path params)+ search params + hash
  • transition:navigate(push/replace/back/forward)
  • side effects:数据预取、权限校验、埋点、滚动恢复

当你用状态机思维审视路由,很多常见坑会自然消失:

  • “多源真相”问题:同一份筛选条件既在 store 又在 URL
  • “不可回放”问题:用户 back 了但 UI 仍保持旧条件
  • “刷新丢状态”问题:条件只存在内存

4. URL 的三种信息:path params / search params / hash

4.1 path params:资源身份(resource identity)

  • /users/:userId
  • 通常表达“你在看哪个资源”

4.2 search params:视图投影(view projection)

  • ?page=2&sort=xxx&keyword=abc
  • 通常表达“你以什么视角/条件在看资源集合”

架构建议:筛选/分页/排序优先放 search params。

4.3 hash:页面内定位/锚点(少量使用)

  • #comments
  • 对 SPA 来说更多用于滚动定位;业务状态不要滥用 hash

5. 路由与数据层的协作:URL 是条件真相,Query 是数据真相

把上一章的数据层结论落到路由:

  • URL state(条件):keyword/page/sort/filters
  • Query cache(结果):products list / product detail

正确姿势是:

  1. URL 变化 → 派生 queryKey
  2. queryKey → 读取/请求数据(TanStack Query 或 Next server fetch)

这样保证:

  • 同一 URL 必然命中同一缓存 key
  • 预取能命中
  • back/forward 能回放

6. 导航体验三件套:prefetch / skeleton / transition

6.1 prefetch:把等待前移

  • hover 链接预取
  • 进入页面前预取(路由 loader / server fetch)

6.2 skeleton:把等待变“可理解的占位”

  • route-level skeleton:页面壳
  • section-level skeleton:列表区/详情区

6.3 transition:把非紧急更新降级,尽量保留旧 UI

  • 切页/筛选常常是 Transition 候选
  • 避免导航时频繁掉到 fallback(闪屏)

这三者分别解决:等待时间、视觉反馈、交互响应性。

7. 两套技术栈下如何落地这套心智模型?

7.1 TanStack Router + Vite(客户端路由)

落地要点:

  • URL state:用 search params 表达,并用 schema 定义默认值与类型(保证链接可还原)
  • route loader:适合作为“进入页面前的准备阶段”(鉴权、prefetch)
  • 与 TanStack Query 协作:
    • loader 里 queryClient.prefetchQuery(queryKey)
    • 组件里 useQuery(queryKey) 直接命中

7.2 Next.js App Router(全栈路由)

落地要点:

  • URL state:searchParams 是天然的条件来源
  • segment 级边界:
    • loading.tsx 负责“等”
    • error.tsx 负责“错”
    • not-found.tsx 负责“无”

并且要把“数据在哪取”先定清:

  • Server Components 取数:更像框架级缓存与重验证(注意一致性策略)
  • Client Components 取数:可用 TanStack Query 管理交互数据

8. 常见反模式

  1. 把筛选条件复制到全局 store
  • 结果:URL 与 store 不一致;back/forward 不可回放;bug 难排
  1. 把“视图语义”塞进 path
  • 例如把 sort/page/filter 都做成 path 段,导致路由爆炸与维护困难
  1. 导航时整页 fallback(白屏)
  • 结果:用户感知差;INP 变差
  • 解决:保持稳定 shell,切分 Suspense/Loading 边界,必要时配合 Transition

9. Checklist(你设计一个页面/模块路由时自检)

  • 哪些状态必须可分享/可回放?是否进 URL?
  • URL 中的 search params 是否有 schema/默认值/类型约束?(保证可还原)
  • 数据获取边界在哪里?(loader / server fetch / client fetch)
  • 是否有预取策略?是否能命中缓存?
  • loading/error/not-found 是否分层?是否保持稳定 shell?

关联阅读

cd ..