前言
在单页应用(SPA)席卷前端领域的今天,网页早已不再是"点击链接→等待刷新"的传统模式。当我们在浏览器地址栏输入URL时,页面竟能魔术般地切换内容而不刷新------这背后究竟藏着怎样的技术奥秘?
前端路由正是实现这种"魔法"的核心技术。它像一位聪明的"URL翻译官",在浏览器地址变化与页面组件展示之间搭建桥梁。无论是电商平台的商品分类切换,还是管理系统的菜单导航,都离不开路由机制的支撑。本文将带您揭开前端路由的神秘面纱,通过hash模式与history模式两大实现方案的对比,结合手写代码案例,彻底理解这项构建现代Web应用不可或缺的技术。
什么是路由?
用来描述服务器上资源的路径
前端路由是指?
-
单页应用中
-
构建 浏览器 url 地址 和 组件之间的映射关系
前端路由的本质
- 路由是浏览器URL地址与前端组件之间的映射关系,充当"媒婆"的角色,实现URL变化时展示对应的页面组件。
前端路由实现的要求
- 要监听 url是否 变更了
- url变更时,浏览器不能刷新(单页应用,当浏览器刷新时,整个应用都会重新加载)
hash 模式
在浏览器眼里,url 携带了#,#后面的内容都会被认为是 hash 值,浏览器中 hash 值的变更不会带来页面变更
原理:通过对 url 设置监听,当 hash 值改变时,触发 hashchange 事件,使用 location.hash 获取 url 中#后面的 hash 值,根据 hash 值,找到数组对象中对应的组件,将组件渲染到页面中
手搓hash
我们首先需要监听'haschange'事件,其中的事件参数包含了我们需要监听的url地址,但是官方也为url地址栏专门打造了对象location,location.hash
js
window.addEventListener("hashchange", (e) => {
console.log(e);
renderView(window.location.hash);
});
通过监听事件对象里面的url
js
function renderView(url) {
const index = routes.findIndex((item) => {
return "#" + item.path === url;
});
let routerView = document.getElementById("root");
routerView.innerHTML = routes[index].component();
}
这里相当于发布订阅模式中的订阅,当点击url发生变更的时候就相当于执行发布中的renderView函数,函数将,通过在routes数组里面找是否有对应的url地址,有就返回数组中对应路径的下标,然后再将数组里面对应的那个组件代码片段取出来出来展示。展示在这里div标签,通过设置这个div标签的innerHTML
js
window.addEventListener("DOMContentLoaded", () => {
renderView(window.location.hash);
});
增加一个"DOMContentLoaded"事件监听在页面初次刷新时进行组件的初始化
js
<!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><a href="#/home">首页</a> <a href="#/about">关于</a></li>
</ul>
<!-- 当url变更后展示对应的代码片段 -->
<div id="root"></div>
<script>
const routes = [
{
path: "/home",
component: () => {
return "<h1>首页e</h1>";
},
},
{
path: "/about",
component: () => {
return "<h1>关于e</h1>";
},
},
];
window.addEventListener("hashchange", (e) => {
console.log(e);
renderView(window.location.hash);
});
function renderView(url) {
const index = routes.findIndex((item) => {
return "#" + item.path === url;
});
let routerView = document.getElementById("root");
routerView.innerHTML = routes[index].component();
}
window.addEventListener("DOMContentLoaded", () => {
renderView(window.location.hash);
});
</script>
</body>
</html>
history 模式
浏览器提供了一个 history 对象,用来管理浏览器的历史记录,并且 history 对象中有 pushState、replaceState、popState 三个方法,通过调用这三个方法
pushState:向历史记录栈中添加一条记录
replaceState:替换当前历史记录栈中的记录
popState:向历史记录栈中取出最后一条记录
pushState 修改 url 不触发页面刷新
我们可以改变 url,并触发 popState 事件,从而实现路由跳转。即监听popState事件可以关联到浏览器的前进后退事件
window.history.pushState(null, "", item.getAttribute("href"));
注意:.getAttribute(), 这个叫读取标签身上的属性。任何标签都可以用getAttribute去读取它身上的任何属性
window.history.pushState(null, "", item.getAttribute("href"));
renderView(location.pathname)
但我们pushState改变了url之后通过渲染函数,渲染对应url的组件,怎么知道当前url路径,可以通过事件参数也可以通过location.pathname来得到对应的url
pushState()会进入浏览器的缓存栈中但是不会被前进和后退按钮事件触发,hash模式和多页应用在跳转了页面之后,跳转到的路径是会被浏览器的缓存站给缓存起来,拥有历史记录。但用pushstate跳转的url虽然有历史记录,但无法被浏览器的前进后退事件触发,后退无法回到pushState添加的url地址,而是回到pushState之前的url地址。那么我们回退的时候还想要监听回退事件,我们就可以监听history对象身上的popstate事件,单点击回退按钮,会触发,设计一个渲染函数,帮我们渲染对应url的组件
window.addEventListener('popstate', () => { // 监听浏览器的前进后退事件 renderView(location.pathname) })
也就是触发这里的函数调用
html
<!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><a href="/home">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
<!-- 当 url 变更后,展示对应的代码片段 -->
<div id="root"></div>
<script>
const routes = [
{
path: '/home',
component: (val) => {
return `<h1>首页页面${val}</h1>`
}
},
{
path: '/about',
component: () => {
return '<h1>关于页面</h1>'
}
}
]
let routerView = document.getElementById('root')
window.addEventListener('DOMContentLoaded', () => {
onLoad()
})
window.addEventListener('popstate', () => { // 监听浏览器的前进后退事件
renderView(location.pathname)
})
function onLoad() { // 渲染对应的组件
let linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => {
el.addEventListener('click', function(e) {
e.preventDefault() // 阻止默认行为
history.pushState(null, '', el.getAttribute('href')) // 进入浏览器的缓存栈,但是不受前进后退事件的影响
// console.log(location);
renderView(location.pathname)
})
})
}
function renderView(url) {
const index = routes.findIndex(item => {
return item.path === url
})
routerView.innerHTML = routes[index].component()
}
</script>
</body>
</html>
总结
从#号后的哈希值到干净的路径地址,从手动监听hashchange到利用history API优雅控制浏览历史,我们见证了前端路由技术的演进历程。这两种模式如同双生花:
- hash模式以兼容性见长,像一位可靠的老匠人,用#符号在URL中划出专属领地
- history模式则追求极致体验,通过pushState/replaceState实现无刷新导航,宛若现代建筑中的智能电梯