一、什么是路由?
在 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 路由实现步骤
- 定义路由表
js
const routes = [
{
path: '/home',
component: () => '<h1>首页页面</h1>'
},
{
path: '/list',
component: () => '<h1>列表页页面</h1>'
}
]
- 监听
hashchange
事件,当hash
值变更时,触发hashchange
事件,通过location
获取#
和后面的内容。在事件处理函数中,根据hash
值,找到对应组件,将组件渲染到页面中。
js
// 监听 URL 的变化
window.addEventListener('hashchange', (e) => {
renderView(location.hash)
})
function renderView(url){ // #/home
// findIndex() 方法返回数组中满足提供的第一个元素的索引
// 找到路由配置中,path 与 url 匹配的项
const index = routes.findIndex(item => {
return ('#' + item.path) === url
})
// 获取路由出口
let routerView = document.getElementById('root')
// 路由出口中,展示路由组件
routerView.innerHTML = routes[index].component()
}
✅ 优点:实现简单,兼容性好(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 中的路径。
js
// popstate 事件监听浏览器前进/后退
window.addEventListener('popstate', () => {
renderView(location.pathname)
})
- 页面加载完成时,调用
onLoad
方法,拦截<a>
标签默认行为。
onLoad
方法:找到所有的链接,给每个链接添加点击事件;click
点击事件:阻止默认事件,修改 URL,调用renderView
方法。
js
window.addEventListener('DOMContentLoaded', () => {
onLoad()
})
function onLoad() {
// 找到所有的链接
let linkList = document.querySelectorAll('a[href]')
// 遍历所有的链接
linkList.forEach(item => {
// 给每个链接添加点击事件
item.addEventListener('click', (e) => {
// 阻止默认事件
e.preventDefault()
// pushState() 进入浏览器的缓存栈,但是不触发 popstate 事件,不受前进后退影响
history.pushState(null, '', item.getAttribute('href'))
renderView(item.getAttribute('href'))
})
})
}
- 定义
renderView
方法渲染对应的组件。
js
function renderView(url) {
let index = routes.findIndex(item => {
return item.path === url
})
routerView.innerHTML = routes[index].component()
}
✅ 优点: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) |
实现复杂度 | 低 | 中 |