前言
什么是路由?
想象你开车去一个陌生城市,手里拿着地图导航:
-
目的地 = 你要访问的网站(比如淘宝/微信)
-
十字路口 = 网络中的路由器
-
路牌指示 = 路由规则
路由就是网络世界的导航系统:
-
你在浏览器输入
taobao.com(告诉导航要去"淘宝大厦") -
路由器(路口交警)查看"地址簿"(路由表):
- "淘宝大厦 → 走3号高速出口"
- "微信大厦 → 走5号隧道"
-
数据包像快递车一样,经过多个路口(路由器),每个路口根据"地址"决定下一站方向
-
最终准确到达淘宝服务器(目的地)
核心作用:把网络请求精准送到目标服务器,就像快递分拣站根据地址分发包裹
那什么是前端路由呢?
想象一本魔法相册:
-
相册本身 = 你的浏览器页面(始终不刷新)
-
照片 = 不同页面内容(首页/购物车/个人中心)
-
目录标签 = 浏览器地址栏的URL(如
site.com/#cart)
传统网站(无前端路由) :
-
想看购物车?撕掉当前页 → 电话联系印刷厂(服务器)→ 等新的一页寄过来 → 贴到相册里
-
每次都要重印整本相册(页面刷新)
前端路由的魔法相册:
-
相册自带所有照片(首次加载全部页面资源)
-
点"购物车"标签时:
- 悄悄改地址栏:
site.com/#cart(但不相册翻页) - 瞬间从夹层抽出购物车照片覆盖当前页(内容切换)
- 悄悄改地址栏:
-
点"个人中心"标签时:
- 地址栏变
site.com/#profile - 快速抽出个人中心照片覆盖
- 地址栏变
-
关键魔法:相册本身始终没换!只是快速抽换内页照片
核心技术:
Hash模式:用 #cart 这类锚点(像书签页签)
History模式:用 /cart 伪装成新页面(地址栏更整洁)
通俗点来讲就是,一直保持一个html页面,当浏览器url地址发生改变时,更换对应的组件展示在页面上(不触发页面刷新),前端路由就是用来构建url地址与组件之间的映射关系的
在了解前端路由的两种模式之前,我们需要知道实现前端路由的两大问题:
1. 怎么知道url地址变更了?
2. 怎么实现修改url(切换组件),浏览器不能刷新?
Hash模式
什么是Hash模式?
想象一本 带便利贴的书:
-
书本身 = 你的网站(
index.html) -
便利贴位置 = URL 中
#后面的部分(如site.com/#page2) -
翻书规则:
-
你想看第二章 → 直接翻到贴了
#page2便利贴的那页 -
书不用重新买(页面不刷新)
-
别人复制
site.com/#page2给你 → 你也能精准翻到那页
-
-
秘密机制:
-
浏览器无视
#后面的内容发给服务器 → 永远只拿到整本书(index.html) -
前端路由偷偷看便利贴(监听
#变化) → 展示对应章节(组件)
-
简单来说就是,浏览器不会将#后面的任何内容发送给后端,所以在url地址上加上#,后面再加任何内容都不会触发浏览器的刷新,那么这就解决了第二个问题,之后便通过hashchange事件监听url的变更,然后替换对应的组件即可。
如何实现呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<!-- href属性使用hash模式,以#开头 -->
<a href="#/home">首页</a>
</li>
<li>
<a href="#/about">关于</a>
</li>
</ul>
<!-- 用于展示路由对应的组件内容的容器 -->
<div id="root"></div>
</body>
<script>
// 定义路由配置数组
const routes=[
{
path:'/home', // 路由路径
component:()=>{ // 路由对应的组件(函数形式返回HTML字符串)
return '<h1>首页</h1>'
}
},
{
path:'/about',
component:()=>{
return '<h1>关于</h1>'
}
}
]
// 监听hashchange事件,当URL中的hash部分变化时触发
window.addEventListener('hashchange',()=>{
// 调用renderView函数,传入当前hash值
renderView(location.hash)
})
// 根据传入的URL hash值渲染对应的组件
function renderView(url){
// 在routes数组中查找匹配的路由
// 注意:这里需要将item.path前面加上#进行比较,因为location.hash包含#
const index=routes.findIndex(item=>{return '#'+item.path===url})
// 如果找到匹配的路由
if(index!==-1){
// 将对应组件渲染到root容器中
document.getElementById('root').innerHTML=routes[index].component()
}
}
// 监听DOMContentLoaded事件,确保页面加载完成后执行
window.addEventListener('DOMContentLoaded',()=>{
// 页面加载完成后,根据当前hash值渲染初始视图
renderView(location.hash)
})
</script>
</html>
使用a标签,当用户点击a标签时便可将url地址更换,再监听hashchange事件,获取当前url地址中#后面的值(包括#),然后再将获取的hash值拿去与routes数组进行匹配,找到对应的组件并渲染,最后监听DOMContentLoaded事件,只有当页面 DOM 完全就绪后,才会根据当前 URL 的 hash 值来渲染对应的组件内容到 #root 容器中,确保页面加载完成后,立即根据当前 URL(或 hash)渲染对应组件,保证用户打开页面时能看到正确的初始内容。
History模式
什么是history模式?
想象一个 会魔法的智能写字板:
-
写字板本体 = 你的网站(
index.html) -
板上的字 = URL 中的路径(如
site.com/cart) -
魔法操作:
-
你想写
/cart→ 用魔法笔一挥,板子瞬间变成购物车页面 -
板子还是那块板子(页面不刷新)
-
地址栏显示
site.com/cart(像全新页面! )
-
-
风险点:
-
如果直接访问
site.com/cart→ 浏览器会真去服务器找cart.html(但服务器没有!) -
需要提前告诉服务器: “所有路径都给我
index.html!” (否则报错 404)
-
history模式和hash模式的区别之一就是history模式没有了#,使得url地址变得更美观了但同时也带来了一个问题,history模式下的url地址和传统地址相像,当用户直接访问这种url地址时(如example.com/about ) ,浏览器会向后端请求该路径下的资源,但服务器并没有该路径的资源(没有about.html),服务器只有主页面html(index.html)的资源,所以会报错404,这时候就需要配置服务器,告诉服务器,无论什么地址的请求,都返回主页面html的资源,然后由前端路由根据不同的url地址加载对应组件。
如何实现呢?
hash模式可以改变#后面的url地址,达到页面不刷新的效果,但是history模式没有#号,而是和普通路径一样,所以只能通过浏览器提供的history对象,调用它的pushState方法将url地址添加到浏览器历史记录栈中,同时也不触发页面的更新,还需要绑定popstate事件,当用户点击浏览器的前进和后退按钮时,浏览器便被通知历史记录发生变更,需要重新根据url地址更换对应的组件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<!-- 导航链接,使用History模式(无#前缀) -->
<a href="/home">首页</a>
</li>
<li>
<a href="/about">关于</a>
</li>
</ul>
<!-- 用于展示路由对应的组件内容的容器 -->
<div id="root"></div>
</body>
<script>
// 定义路由配置数组
const routes=[
{
path:'/home', // 路由路径
component:()=>{ // 路由对应的组件(函数形式返回HTML字符串)
return '<h1>首页</h1>'
}
},
{
path:'/about',
component:()=>{
return '<h1>关于</h1>'
}
}
]
// 获取用于渲染组件的DOM容器
const renderView=document.getElementById('root')
// 页面加载完成后,初始化路由
window.addEventListener('DOMContentLoaded',()=>{
onLoad()
})
// 监听popstate事件,处理浏览器前进/后退操作
window.addEventListener('popstate',()=>{
// 根据当前URL路径切换组件
routerView(location.pathname)
})
// 初始化函数,设置导航链接的点击事件
function onLoad(){
// 获取页面上所有带有href属性的a标签
let linkList=document.querySelectorAll('a[href]')
// 遍历所有a标签
linkList.forEach(el=>{
// 为每个a标签添加点击事件监听
el.addEventListener('click',(e)=>{
// 阻止a标签的默认跳转行为(防止页面刷新)
e.preventDefault()
// 使用pushState修改URL,添加新的历史记录
// el.getAttribute('href')获取a标签的href属性值
history.pushState(null,'',el.getAttribute('href'))
// pushState会进入浏览器的历史栈中,使前进/后退按钮有效
// 但是不会触发popstate事件,所以需要手动调用routerView函数
// 根据新的URL路径切换组件
routerView(location.pathname)
})
})
}
// 根据URL路径切换对应的组件
function routerView(url){
// 在routes数组中查找匹配的路由
const index=routes.findIndex(item=>{return item.path===url})
// 如果找到匹配的路由
if(index!==-1){
// 调用组件函数并将结果渲染到容器中
renderView.innerHTML=routes[index].component()
}
}
</script>
</html>
-
在单页应用(SPA)中,虽然a标签可实现页面跳转,但会导致整页刷新。因此首先需阻止其默认跳转行为(通过 e.preventDefault() ),使其仅作为交互触发元素而非传统跳转标签。
-
获取a标签的 href 属性值(使用 el.getAttribute('href') ),该值即为目标跳转的URL路径。
-
使用 history.pushState(null, '', targetUrl) 方法更新浏览器URL地址,此操作不会触发页面刷新,但会将新URL添加到浏览器历史记录栈中,使前进/后退按钮可用。
-
将获取到的URL路径与预定义的 routes 数组进行匹配(通常根据 path 字段),找到对应的路由配置项,并获取其关联的组件函数。
-
调用组件函数生成HTML内容,并将其渲染到页面指定容器(如 root 元素)中,完成视图更新。
-
最后,需监听 popstate 事件以处理浏览器的前进/后退操作。当用户点击前进/后退按钮时,会触发该事件,此时需重新调用路由匹配渲染函数(如 routerView ),根据当前URL路径切换显示对应的组件。