本节摘要:路由是 SPA 的"页面导航系统",它让"无需刷新切换页面"成为可能。本节讲清两种历史模式的原理与取舍(hash 免配置但丑,history 干净但依赖服务器配合),再展开嵌套路由、动态路由、路由守卫与权限控制、懒加载、状态持久化等实战主题。你会得到一个"按项目需求选路由模式 + 设计权限体系"的完整方法。
阅读完本节,你应当能够:
传统网站每个 URL 对应一个 HTML 文件,点链接整页刷新。SPA 只有一份 HTML,剩下的都是 JavaScript 渲染——那么问题来了:用户点"设置"页,浏览器地址栏怎么变?变了之后刷新,怎么还回到"设置"页?这些问题,就是路由要解决的。
路由的职责有三层:一是地址与页面映射——URL 变了,显示对应的组件;二是历史记录——浏览器前进后退按钮正常可用;三是导航控制——跳转前能不能拦截(比如未登录不能进后台)。这三层需求催生了 hash 与 history 两种模式,也催生了"守卫"这套权限机制。
💡 关键直觉:路由本质是"把浏览器地址变成应用状态的一部分"。理解了这一点,你就知道为什么刷新要能恢复页面、为什么权限要在跳转前拦截——这些都是"地址即状态"的直接推论。
# 后的片段做路由URL 长这样:「相关地址请参见官方文档」 之后的变化不会触发服务器请求,浏览器也不会整页刷新——SPA 只需监听 hashchange` 事件即可完成切换。
优点:无需服务器配置,任何静态托管都能跑,部署最简单。缺点:URL 带 # 不美观、不利于 SEO(爬虫对 hash 内容收录不佳)、分享链接稍显奇怪。
URL 长这样:「相关地址请参见官方文档」 pushState/replaceState改变地址,配合popstate` 监听前进后退。
优点:URL 干净、SEO 友好。缺点:强依赖服务器配合——用户直接访问 /user/profile 时,服务器必须把所有路径都重写回入口 HTML,否则 404。这一条是 history 模式最常见的"上线翻车点"。
| 模式 | URL 形态 | 服务器要求 | SEO | 适用场景 |
|---|---|---|---|---|
| hash | /#/path |
无 | 差 | 内网工具、演示环境、快速原型 |
| history | /path |
需重写规则 | 好 | 对外产品、需 SEO 的 SPA |
<Route>,Vue Router 用 children 配置,Angular 用子路由数组。/user/:id、/post/:slug),同一组件根据参数渲染不同数据。三框架都支持在路由配置里声明参数。路由守卫解决"某些页面只有特定角色能进":在跳转前做检查,不合格就重定向到登录页或 403 页。
beforeEach、路由独享守卫、组件内守卫。全局守卫最常用——在 beforeEach 里查登录态与角色,决定放行或重定向。<RequireAuth> 组件,检查权限后决定渲染目标组件还是重定向。CanActivate 守卫接口,返回 true/false 或 Observable,控制路由放行。() => import(...) 动态导入、React Router 配合 React.lazy + Suspense、Angular 的 loadChildren。这是 SPA 首屏优化最有效的手段之一(详见 5.6)。先问三个问题。一、项目要不要被搜索引擎收录?要 → history + SSR/SSG 组合(详见 5.4)。二、部署环境能否配置服务器重写规则?不能 → 只能 hash。三、是否需要干净的分享链接?要 → history。大多数对外产品走 history,内网工具走 hash 更省事——别为"美观"硬上 history,然后被 404 折腾。
⚠️ 常见坑:只做前端路由守卫就以为"安全了"。路由守卫可以被绕过(直接改 JS 或调 API),它拦截的是"普通用户误入",不是"恶意用户入侵"。后端接口必须独立做权限校验。
| 传参方式 | 适用 | 例子 |
|---|---|---|
| 路径参数 | 标识资源 | /user/42 |
| 查询参数 | 筛选条件 | /list?page=2&sort=time |
| 状态对象 | 瞬态数据 | 列表页 → 详情页传"来源"标记 |
原则:能在路径表达的就放路径,能放 URL 查询的放查询——这样刷新、分享、前进后退都自然可用。
路由配置集中管理(一个路由表文件),别散落在各页面。集中后:权限表、懒加载、嵌套结构、菜单生成都能从同一份配置推导——路由表是你的"应用地图",维护好它,等于维护好了应用的导航系统。
一个高频的设计问题:页面的筛选条件、分页、标签页选中项,该放组件状态还是放路由?经验法则是:"能被分享、能被回退"的状态放路由,其余放组件状态。比如后台列表页的筛选条件——用户希望刷新后还在、希望复制链接分享给同事看到同样的结果,就该放到 URL 查询参数里;而某个表单的临时输入草稿,不必进 URL。把"可分享性"作为判据,很多路由与状态的纠缠会迎刃而解。过度把状态塞进路由的代价是 URL 越来越长、解析复杂度上升;完全不放的代价是刷新丢失、无法分享。取一个平衡点,通常是"关键筛选条件进 URL,草稿类状态留本地"。
用户在一个多层级的后台系统里(工作台 → 项目管理 → 项目详情 → 任务列表),很容易迷路。面包屑是导航系统的"最后一块拼图",实现上有个反直觉的要点:面包屑不该从 URL 逐级猜,而该由路由配置显式声明。因为 URL 的层级(/project/12/task)不一定对应业务层级(项目 → 任务),且有些页面(弹窗路由、详情页)不该出现在面包屑里。做法:在路由配置里为每个路由标注"面包屑文案"与"父级引用",由统一的组件递归渲染。这套做法同时带来一个好处:菜单的高亮定位也能从同一份路由配置推导——导航、菜单、面包屑共用一份"路由元数据",不会出现"菜单说 A、面包屑说 B"的错乱。导航系统做到这个程度,才算真正闭环。
页面切换加一点过渡动画,能让应用显得更"顺滑专业",但这里有一个容易走偏的点。好的过渡:页面切换有明确的"进入/退出"语义(左滑进入、淡入淡出),时长控制在 200-400 毫秒,不干扰用户操作。走偏的过渡:动画时间过长(用户等得烦躁)、每个页面都大动干戈(视觉疲劳)、或动画与数据加载互相干扰(内容没到动画先播完,看起来像卡顿)。实现方式上,Vue Router 有内置的 <transition> 支持,React 需要第三方方案(如 framer-motion 或 CSS 过渡 + 路由 key),Angular 有 Angular Animations。给一个务实建议:默认不做动画,只在"需要传达层级关系"的地方做(比如详情页从列表页进入时的右滑,明确"我是从哪来的")。动画是体验的调味品,放多了会盖住主菜的味道。
把前几节的内容串成一个完整后台的导航设计:路由表是唯一数据源,每个路由声明"标题、图标、权限角色、是否入菜单、面包屑父级";菜单与面包屑都由路由表生成;守卫根据角色过滤可访问路由,同时过滤菜单显示——用户没权限的路由,既不能访问也不该显示在菜单里。这套设计的关键收益是"单点修改":新页面只需在路由表加一行,菜单、面包屑、权限自动就绪;改权限只需改路由表的角色字段。相反,如果菜单、权限、路由各写一套,任何新增页面都要改三处,漏改一处就出"菜单有、点进去 404"或"没权限却看得到入口"的错乱。把路由表当成"导航系统的唯一事实来源",是后台类项目最重要的架构决策之一——它把最容易散乱的三个部分(导航、权限、面包屑)收敛成了单一配置。
后台应用的两个高频痛点,这里给排查思路。痛点一:刷新后"状态丢了"——刷新后回到首页而不是原页面,或筛选条件清空。原因通常是"页面状态放在组件内存里,没同步到 URL"。解法:把关键状态(当前 Tab、筛选条件、分页)同步到 URL 查询参数(见 3.6),刷新后从 URL 恢复。痛点二:浏览器的"返回"跳错页面——从 A 进 B 再进 C,点返回却跳到别处。原因通常是"非路由方式改变内容"(比如用状态切换 Tab 而不是路由切换),历史记录与实际视图脱节。解法:凡是"用户会期望浏览器能返回"的切换,都应该走路由而不是状态——让历史记录与页面视图保持一致。这两个痛点的根子其实是同一个原则:页面状态要与 URL 绑定,浏览器历史与视图才不脱节。把这条原则写进团队的开发约定,能消灭一大类"导航体验诡异"的 Bug。
路由把"页面怎么走"理顺了,下一步是"页面长什么样"——UI 组件库与设计系统。