实现前端路由hash、history模式没那么简单
“作为单身狗一枚,实在不知道情人节干什么😂,哎算了,踏踏实实写文章吧。
”
hash模式:
这个模式比较简单,当我们修改一个html文件的hash时,也就是URL地址后面加一个#value
浏览器是不会发送请求的,且即使发送请求(如刷新页面)也不会携带上hash,利用这个特性我们可以通过监听hash的变化从而根据#后面的参数操作DOM完成相应的页面跳转。
这里着重了解三个地方:
修改hash通过a标签就可以完成, <a href="#1">显示1</a>
监听hash的改变通过hashchange事件监听 通过window.location.hash获取hash
逻辑代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#page1,#page2,#page3,#page4,#page404 {
display: none;
}
</style>
</head>
<body>
<a href="#1">显示1</a>
<a href="#2">显示2</a>
<a href="#3">显示3</a>
<a href="#4">显示4</a>
<hr>
<div id="app">
</div>
<div id="page1">1</div>
<div id="page2">2</div>
<div id="page3">3</div>
<div id="page4">4</div>
<div id="page404">404 Not Found</div>
<script>
// 获取所有的页面
let page1 = document.querySelector('#page1');
let page2 = document.querySelector('#page2');
let page3 = document.querySelector('#page3');
let page4 = document.querySelector('#page4');
// 建立hash表
const routeTable = {
'1': page1,
'2': page2,
'3': page3,
'4': page4
}
function route() {
// 获取当前hash
let number = window.location.hash.slice(1);
number = number || 1;
// 根据hash拿到当前页面
let page = routeTable[number.toString()];
page = page || document.querySelector('#page404');
page.style.display = 'block';
// 拿到容器
let app = document.querySelector('#app');
if(app.children.length > 0) {
app.innerHTML = '';
}
// 渲染页面
app.appendChild(page);
}
// 初始渲染主页面
route();
// 监听hash的变化
window.addEventListener('hashchange',function(){
route();
})
</script>
</body>
</html>

其兼容性是比较好的,但是由于修改hash不会向浏览器发送请求,所以SEO不友好。
hastory模式:
在之前我了解到的history模式无非就是通过history.pushState
或者history.replaceState
修改URL,然后根据MDN文档给出的会触发popstate
事件,我们只要对popstate事件进行监听获取location.pathname
像上面一样根据对应的参数修改页面的内容就好了。
之后我在网上搜索了一下,对于修改URL但不发送请求的方法主要是window.history上面的:
back():后退到上一个路由; forward():前进到下一个路由,如果有的话; go(number):进入到任意一个路由,正数为前进,负数为后退; pushState(obj, title, url):前进到指定的 URL,不刷新页面; replaceState(obj, title, url):用 url 替换当前的路由,不刷新页面;
区别是前三个主要是利用浏览器的历史记录,并不能生成新的URL,但后面两个会让浏览器将新的URL存入历史记录。
但是还要注意history模式必须与后端相配合,因为虽然history.pushState
在修改URL后页面不会重新加载,但如果我们刷新页面还是会用新的URL去发送请求的,如果此时后端的URL还没有更新那么便会返回404
。
接下来详细说一下history.pushState
这个API,其作用是在浏览器中添加一条新的历史记录,但不刷新页面。且其是在同源情况下进行的,也就是说其只会在当前URL后添加一下新的内容,这里是MDN的文档[1]大家可以详细去看一下,上面也明确的说了history.pushState
会触发popstate
这个事件。
忽略前两个参数,我们主要需要关注第三个参数url便好,这是比较基本的用法:
window.history.pushState(state,title,url)
state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取 title:标题,基本没用,一般传null url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以是绝对路径,也可以是相对路径。
//如当前url是 https://www.baidu.com/a/,
//执行history.pushState(null, null, './qq/'),
//则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),
//则变成 https://www.baidu.com/qq/
通过这个我们可以发现pushState
的第三个参数可以是相对的地址。
说到这里我想对于history模式大家应该有一个大致的了解了,以为文章就这么欢快的结束了?但是如果一切如愿也就没有发这篇文章的意义了。
接着我去尝试着向上面的hash模式一样写一个简单的案例,我知道自己没有后端配合,但是我想只要调用pushState
方法可以触发popstate
事件便好了,证明这样是可以完成任务的,但是。。。
history.pushState(null,null,'#333');
window.addEventListener('popstate',function(){
console.log(location.pathname);
})
我写了一个小小的案例,按道理如果我通过pushState
修改的hash
的话,刷新页面后不是会发送请求的但会触发popload事件,从而获取到对应的pathname。但是我蒙了,控制台啥也没有输出???
我想这难道就是新年的第一个BUG吗?于是我只能在网上继续进行苦逼的搜索,终于找到了一篇文章和我遇见了同样的问题。
原来虽然popstate
事件也带着一个state但是,其是无法通过pushState
与replaceState
触发的,但是go(),back(),forward()
却可以触发。
于是我立刻去进行了实验:
window.addEventListener('popstate',function(){
console.log(location.pathname);
})
setTimeout(function(){
history.back();
},1000)
发现这回真的成功了,但是如果popstate无法监听pushState对URL的修改那么,我们究竟如何实现对pushState的监听呢?接下来真正的重点来了。。。
我们需要创建一个自定义事件new Event('pushState')
,当我们调用pushState方法时去手动触发我们自定义的事件,然后对自定义事件进行监听就可以取代popstate事件了。
对自定义事件不了解的同学可以看MDN Event[2],其实将其理解为我们自己定义的一个事件就可以了,像click之类的,有了事件之后我们就可以将其绑定在一个元素上了,这个例子我先通过addEventListener
绑定到window
上。
绑定之后我们便需要触发绑定的事件了,我们绑定到了什么上我们就需要通过什么元素来触发,触发事件的API是dispatchEvent
。其和我们操作DOM触发事件不同的地方在于其是同步的。具体也可以看MDN dispatchEvent[3]。
let oBox = document.querySelector('.box');
const andyevent = new Event('andyEvent')
window.addEventListener('andyEvent',function(){
console.log('andy创建的事件触发了');
})
setTimeout(function(){
window.dispatchEvent(andyevent);
},1000)
说了这么多这下回到正题,上面这些只是方便大家来理解new Event()
与dispatchEvent()
两个API的作用,之后我们通过重写history.pushState
方法,内部创建一个pushState
自定义事件,当调用pushState
方法时触发我们的自定义事件,在从而在外部监听便好,这是完成的代码。
// 重写pushState与replaceState方法
function addStateListener(){
function listener(type){
let origin = history[type];
return function(){
let newOrigin = origin.apply(this,arguments);
let stateEvent = new Event(type);
// 添加arguments的原因是可以在监听到事件触发时拿到传递给事件的参数
stateEvent.arguments = arguments;
window.dispatchEvent(stateEvent);
return newOrigin;
}
}
history.pushState = listener('pushState');
history.replaceState = listener('replaceState');
}
addStateListener()
实际测试:
addStateListener()
// 监听pushState与replaceState
window.addEventListener('pushState',function(e){
console.log(location.pathname,e.arguments[2]);
})
window.addEventListener('replaceState',function(e){
console.log(location.pathname,e.arguments[2]);
})
// 使用pushState与replaceState修改URL
history.pushState(null,null,'#333')
history.replaceState(null,null,'#666')
好了到这里就真的结束了,后面我们根据location.pathname
去截取字符串,或者直接向上面一样通过arguments
来获取URL被修改部分的参数,来控制页面某些部分的显示与隐藏便好,这里就不再像hash那样具体的展示了。
小狮子有话说
你好,我是 Chocolate,一个狮子座
的前端攻城狮,希望成为优秀的前端博主,每周都会更新文章,与你一起变优秀~
关注 小狮子前端
,回复【小狮子
】获取为大家整理好的文章、资源合集我的博客地址: yangchaoyi.vip
欢迎收藏,可在博客留言板留下你的足迹,一起交流~觉得文章不错,【 点赞
】【在看
】支持一波 ✿✿ヽ(°▽°)ノ✿
叮咚~ 可以给小狮子加
星标
,便于查找。感谢加入小狮子前端,最好的我们最美的遇见,我们下期再见~
参考:
腾讯大佬小蚊子:深入理解前端中的 hash 和 history 路由[4]
大黑豹:hash和history实现以及区别[5]
参考
MDN的文档: https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
[2]MDN Event: https://developer.mozilla.org/zh-CN/docs/Web/API/Event
[3]MDN dispatchEvent: https://developer.mozilla.org/zh-CN/docs/Web/Guide/Events/Creating_and_triggering_events#%E8%A7%A6%E5%8F%91%E5%86%85%E7%BD%AE%E4%BA%8B%E4%BB%B6
[4]深入理解前端中的 hash 和 history 路由: https://zhuanlan.zhihu.com/p/130995492
[5]hash和history实现以及区别: https://www.jianshu.com/p/ae8f9c41a77c