一、什么是路由?
在 Web 开发中,路由(Route) 是 URL 与资源 之间的映射关系。
- 服务端路由:传统多页应用(MPA)里,浏览器每跳转一次 URL,都会向服务器发起一次新的请求,服务器根据 URL 返回不同的 HTML 页面。
- 前端路由:单页应用(SPA)里,浏览器只加载一次
index.html
,后续 URL 变化完全在客户端完成,不触发整页刷新,通过 JS 动态渲染局部视图仅局部更新 DOM,不刷新页面。
二、SPA 为什么需要前端路由?
维度 | 多页应用(MPA) | 单页应用(SPA) |
---|---|---|
URL 与 资源 映射 | 一个 URL 对应一份 HTML | 一个 URL 对应一份组件 |
页面切换 | 整页刷新 | 局部 DOM 替换 |
体验 | 白屏、状态丢失,割裂 | 无缝、沉浸 |
SEO | 天然友好 | 需 SSR/预渲染 |
✅ 结论:前端路由让 SPA 既拥有“不刷新”的流畅体验,又能在浏览器地址栏正确反映当前视图状态。
三、前端路由要解决的两个核心问题
1. 如何感知 URL 变化?
浏览器原生并不会在 URL 变化时主动通知我们,需要手动监听。
- Hash 模式监听
hashchange
- History 模式监听
popstate
2. 如何根据 URL 找到并渲染对应组件?
需要维护一个路由表,路由表中存储了 URL 和组件的映射关系。当 URL 发生变化时,根据路由表找到对应的组件。
四、Hash 模式:最简单的前端路由
4.1 原理
URL 中 #
后面的部分称为 Hash。
#
及之后的内容不会被发送到服务器,因此改变 Hash 不会触发页面刷新。- 浏览器原生事件
hashchange
,用来实时监听 Hash 变化。
4.2 Hash 路由实现步骤
- 定义路由表
1const routes = [
2 {
3 path: '/home',
4 component: () => '<h1>首页页面</h1>'
5 },
6 {
7 path: '/list',
8 component: () => '<h1>列表页页面</h1>'
9 }
10]
- 监听
hashchange
事件,当hash
值变更时,触发hashchange
事件,通过location
获取#
和后面的内容。在事件处理函数中,根据hash
值,找到对应组件,将组件渲染到页面中。
1window.addEventListener('hashchange', (e) => {
2 renderView(location.hash)
3})
4
5function renderView(url){
6
7
8 const index = routes.findIndex(item => {
9 return ('#' + item.path) === url
10 })
11
12 let routerView = document.getElementById('root')
13
14 routerView.innerHTML = routes[index].component()
15}
✅ 优点:实现简单,兼容性好(IE8+)。
❌ 缺点:URL 带#
,不美观;SEO 不友好;服务端无法直接拿到 Hash 部分。
五、History 模式:更优雅的现代方案
5.1 History API 简介
HTML5 新增的 window.history
提供了无刷新操作浏览器历史记录的能力。
方法 | 作用 | 是否触发 popstate |
---|---|---|
pushState(state, title, url) | 新增历史记录 | ❌ |
replaceState(state, title, url) | 替换当前记录 | ❌ |
back / forward / go(n) | 浏览器或 JS 导航 | ✅ |
✅ 注意
- 只有浏览器前进/后退才会触发
popstate
。 - 代码调用
pushState/replaceState
后需手动渲染。
5.2 History 路由实现步骤
- 定义路由表;
- 监听
popstate
事件,当 URL 发生变化时,触发popstate
事件,通过location
获取 URL 中的路径。
1window.addEventListener('popstate', () => {
2 renderView(location.pathname)
3})
- 页面加载完成时,调用
onLoad
方法,拦截<a>
标签默认行为。
onLoad
方法:找到所有的链接,给每个链接添加点击事件;click
点击事件:阻止默认事件,修改 URL,调用renderView
方法。
1window.addEventListener('DOMContentLoaded', () => {
2 onLoad()
3})
4
5function onLoad() {
6
7 let linkList = document.querySelectorAll('a[href]')
8
9 linkList.forEach(item => {
10
11 item.addEventListener('click', (e) => {
12
13 e.preventDefault()
14
15 history.pushState(null, '', item.getAttribute('href'))
16 renderView(item.getAttribute('href'))
17 })
18 })
19}
- 定义
renderView
方法渲染对应的组件。
1function renderView(url) {
2 let index = routes.findIndex(item => {
3 return item.path === url
4 })
5 routerView.innerHTML = routes[index].component()
6}
✅ 优点:URL 干净,支持 SEO,用户体验更好。
❌ 缺点:需要服务端配合(Nginx/Node 配置兜底),IE10+。
六、框架路由源码级对比
特性 | Vue Router | React Router | Angular Router |
---|---|---|---|
模式 | hash / history / abstract | hash / history / memory | hash / history |
懒加载 | () => import() | React.lazy | loadChildren |
守卫/钩子 | 全局 / 路由 / 组件 | 自定义 Hooks | Guards & Resolvers |
嵌套路由 | <router-view> | <Outlet> | <router-outlet> |
数据获取 | beforeEnter , asyncData | loader | resolve |
七、总结
维度 | Hash | History |
---|---|---|
URL 美观度 | ❌ 带 # | ✅ 干净 |
浏览器兼容 | ✅ IE8+ | ⚠️ IE10+ |
服务端配置 | 不需要 | 需要兜底 |
SEO | 差 | ✅ 好(可 SSR) |
实现复杂度 | 低 | 中 |
个人笔记记录 2021 ~ 2025