Open ArthurWangCN opened 1 year ago
a 标签锚点大家应该不陌生,而浏览器地址上 #
后面的变化,是可以被监听的,浏览器为我们提供了原生监听事件 hashchange,它可以监听到如下的变化:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash 模式</title>
</head>
<body>
<div>
<ul>
<li><a href="#/page1">page1</a></li>
<li><a href="#/page2">page2</a></li>
</ul>
<!--渲染对应组件的地方-->
<div id="route-view"></div>
</div>
<script type="text/javascript">
// 第一次加载的时候,不会执行 hashchange 监听事件,默认执行一次
// DOMContentLoaded 为浏览器 DOM 加载完成时触发
window.addEventListener('DOMContentLoaded', Load)
window.addEventListener('hashchange', HashChange)
// 展示页面组件的节点
var routeView = null
function Load() {
routeView = document.getElementById('route-view')
HashChange()
}
function HashChange() {
// 每次触发 hashchange 事件,通过 location.hash 拿到当前浏览器地址的 hash 值
// 根据不同的路径展示不同的内容
switch(location.hash) {
case '#/page1':
routeView.innerHTML = 'page1'
return
case '#/page2':
routeView.innerHTML = 'page2'
return
default:
routeView.innerHTML = 'page1'
return
}
}
</script>
</body>
</html>
history 模式会比 hash 模式稍麻烦一些,因为 history 模式依赖的是原生事件 popstate。
调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
pushState 和 replaceState 都是 HTML5 的新 API,他们的作用很强大,可以做到改变浏览器地址却不刷新页面。这是实现改变地址栏却不刷新页面的重要方法。
包括 a 标签的点击事件也是不会被 popstate 监听。我们需要想个办法解决这个问题,才能实现 history 模式。
解决思路:我们可以通过遍历页面上的所有 a 标签,阻止 a 标签的默认事件的同时,加上点击事件的回调函数,在回调函数内获取 a 标签的 href 属性值,再通过 pushState 去改变浏览器的 location.pathname 属性值。然后手动执行 popstate 事件的回调函数,去匹配相应的路由。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>History 模式</title>
</head>
<body>
<div>
<ul>
<li><a href="/page1">page1</a></li>
<li><a href="/page2">page2</a></li>
</ul>
<div id="route-view"></div>
</div>
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', Load)
window.addEventListener('popstate', PopChange)
var routeView = null
function Load() {
routeView = document.getElementById('route-view')
// 默认执行一次 popstate 的回调函数,匹配一次页面组件
PopChange()
// 获取所有带 href 属性的 a 标签节点
var aList = document.querySelectorAll('a[href]')
// 遍历 a 标签节点数组,阻止默认事件,添加点击事件回调函数
aList.forEach(aNode => aNode.addEventListener('click', function(e) {
e.preventDefault() //阻止a标签的默认事件
var href = aNode.getAttribute('href')
// 手动修改浏览器的地址栏
history.pushState(null, '', href)
// 通过 history.pushState 手动修改地址栏,
// popstate 是监听不到地址栏的变化,所以此处需要手动执行回调函数 PopChange
PopChange()
}))
}
function PopChange() {
console.log('location', location)
switch(location.pathname) {
case '/page1':
routeView.innerHTML = 'page1'
return
case '/page2':
routeView.innerHTML = 'page2'
return
default:
routeView.innerHTML = 'page1'
return
}
}
</script>
</body>
</html>
这里注意,不能在浏览器直接打开静态文件,需要通过 web 服务,启动端口去浏览网址。默认打开的协议是 file 协议,它是不会被 popstate 监听的。
「前端路由」插件比较火的俩框架对应的就是 Vue-Router 和 React-Router ,但是它们的逻辑,归根结底还是一样的。
通过分析「哈希模式」和「历史模式」的实现原理,让大家对前端路由的原理有一个更深刻的理解。