前端路由的本质是维护一份“URL → 页面”的映射:当 URL 变化时阻止浏览器进行整页导航,解析当前地址、匹配路由配置,再渲染对应视图。Vue Router、React Router 等库的核心流程都建立在这套浏览器能力之上。
传统多页应用中,每次导航都会向服务器请求一份新的 HTML:
单页应用(SPA)通常只在首次访问时加载入口 HTML,之后由 JavaScript 接管站内导航:
因此,路由库不只是一个 path → component 字典。完整实现还要处理浏览历史、动态参数、嵌套路由、重定向、导航守卫、懒加载、滚动位置和异步导航竞态。
浏览器原生导航,例如修改 location.href 或点击普通跨页链接,会创建文档请求并重新加载页面。前端路由使用的是两类只改变地址或历史记录、不重新加载文档的能力:
hashchange。pushState、replaceState,并监听 popstate。路由库通常还会拦截站内 <a> 的点击事件,先 preventDefault() 阻止浏览器默认导航,再调用自己的 push()。新标签页、下载链接、外链以及带组合键的点击不应被拦截。
Hash 模式的 URL 形如 https://example.com/#/users/42?tab=profile。# 后的片段标识符不会作为 HTTP 请求路径发送给服务器,因此服务器始终看到的是 /。
location.hash 或调用路由库的 push()。hashchange。location.hash,匹配路由并更新视图。#,不够自然;片段标识符原本还承担页内锚点语义。History 模式使用正常路径,例如 https://example.com/users/42。核心 API 是:
它们会修改地址和浏览器历史栈,但不会触发页面加载,也不会自动触发 popstate,所以路由器在调用后必须主动执行一次导航逻辑。
popstate 主要在用户点击前进、后退,或代码调用 history.go()、back()、forward() 激活另一条历史记录时触发。pushState() 和 replaceState() 自己不会触发它,这是手写路由时最常见的遗漏。
在 /users/42 页面内切换路由时,前端已经加载,可以自行匹配该路径。但直接打开或刷新这个地址时,浏览器会真的请求:
若服务器把它当作文件路径,而该文件又不存在,就会返回 404。SPA 部署需要把未知的前端路由回退到入口 HTML,再由客户端路由接管:
回退规则不能无差别吞掉静态资源和 API 的 404,否则脚本加载失败或接口路径错误可能被伪装成 200 text/html。应先匹配真实静态文件和 /api 等后端路径,只让前端页面路由回退到 index.html。
路由器会把地址拆成 pathname、search、hash,并处理 base 路径、编码、尾斜杠和相对地址。路由匹配通常只使用路径部分,查询参数和 Hash 作为附加路由状态保留。
例如:
可以得到:
声明式路由通常会被编译成可匹配结构:
/users/42/orders/7 匹配后得到的不只是一个组件,而是一条父子记录链及参数:
静态路径通常比动态参数优先,动态参数又比通配符优先。成熟路由库会对路由分支评分或排序,避免声明顺序意外改变匹配结果。
匹配成功后,路由器通常按顺序执行:
守卫的返回结果一般有三类:继续、取消、重定向。要限制重定向次数,避免 /a → /b → /a 形成死循环。
路由器会维护一个可订阅的当前路由状态。URL 变化并完成导航后,它更新该状态;路由出口组件订阅变化,并按照 matched 记录逐层渲染。
这解释了嵌套路由为何既需要嵌套的配置,也需要对应层级的路由出口。
push、replace浏览器历史可以理解为一个记录栈和当前指针:
push:在当前记录后新增一项;适合普通页面跳转,用户可以后退。replace:替换当前项;适合登录重定向、纠正非法参数,避免用户后退到无效页面。back/forward/go:移动历史指针,由 popstate 或 hashchange 通知路由器。如果用户后退后再 push 新地址,原先的“前进”记录会被丢弃,这与浏览器普通导航一致。
路由懒加载本质是动态 import():构建工具把页面模块拆成独立 chunk,导航到该路由时才下载。
它能减小首屏包体积,但会把部分成本推迟到导航阶段。常见优化包括加载状态、错误重试,以及对高概率下一页使用 prefetch。
异步过程还会产生竞态:用户先进入 A,A 的数据尚未返回就跳到 B;如果 A 最后才完成并提交状态,页面可能被旧导航覆盖。路由器需要为每次导航生成唯一标识或使用 AbortController,提交前确认它仍是最新导航:
| 模式 | 地址来源 | 变化通知 | 是否需要服务端回退 | 典型场景 |
|---|---|---|---|---|
| Hash | location.hash | hashchange | 否 | 静态托管、旧环境、无法配置服务器 |
| History | location.pathname 等 | popstate;pushState 后主动更新 | 是 | 常规 SPA、需要自然 URL |
| Memory | 内存中的记录数组 | 路由器内部通知 | 否 | 测试、React Native、非浏览器环境 |
Memory 路由不读取地址栏,刷新后状态也不会天然保留。它不是浏览器 SPA 的常规部署模式。
在纯 SPA 中,服务端通常只负责返回入口 HTML,客户端路由负责后续页面切换。
在 SSR 或同构应用中,首次请求 /users/42 时,服务端也需要使用同一套路由语义完成匹配、取数并输出 HTML;客户端加载后再“接管”页面,之后的站内导航才走客户端路由。如果服务端和客户端的匹配结果不同,就可能产生 hydration 不一致。
因此,History API 只解决“如何改变和感知 URL”,并不自动提供 SSR。SSR 还需要服务端路由匹配、数据加载、HTML 渲染以及客户端接管。
pushState() 会触发popstate 吗?不会。路由库调用 pushState() 后要主动匹配并渲染;popstate 用来感知历史记录被激活。
不能。前端守卫只能控制界面导航,代码和请求都可能被绕过。接口必须在服务端校验身份与权限。
只有 Hash 的变化不会被发送给服务器;首次文档、脚本、样式和接口请求仍然正常发生。
开发服务器通常默认提供 SPA fallback,生产服务器若没有配置同样的回退规则,深层路径就会被当作真实文件请求。
不一定。框架可能复用相同组件实例,路由缓存也可能保留页面。依赖路由参数的数据和副作用,应监听参数变化并在合适时机清理。
前端路由的本质是让 URL 成为应用状态的一部分,并维护 URL 到视图的映射。Hash 模式通过改变片段标识符并监听
hashchange工作,片段不会发送给服务器,所以无须深层路径回退;History 模式通过pushState、replaceState修改历史记录,用popstate感知前进后退,URL 更自然,但直接访问深层路径时需要服务器回退到入口 HTML。路由器在此之上还会完成 URL 解析、路由匹配、参数提取、守卫与重定向、异步组件加载、竞态取消、状态提交和嵌套视图渲染。前端守卫只改善导航体验,真正的权限校验必须由服务端完成。