随着 ajax 的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是spa的兴起,前端路由系统随之开始流行。
从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下):
- 记录当前页面的状态(保存或分享当前页的url,再次打开该url时,网页还是保存(分享)时的状态);
- 可以使用浏览器的前进后退功能(如点击后退按钮,可以使页面回到使用ajax更新页面之前的状态,url也回到之前的状态);
作为开发者,要实现这两个功能,我们需要做到:
- 改变url且不让浏览器向服务器发出请求;
- 监测 url 的变化;
- 截获 url 地址,并解析出需要的信息来匹配路由规则。
我们路由常用的hash模式和history模式实际上就是实现了上面的功能。
hash模式
这里的 hash 就是指 url 尾巴后的 # 号以及后面的字符。这里的 # 和 css 里的 # 是一个意思。hash 也 称作 锚点,本身是用来做页面定位的,她可以使对应 id 的元素显示在可视区域内。
由于 hash 值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件,浏览器的进后退也能对其进行控制,所以人们在 html5 的 history 出现前,基本都是使用 hash 来实现前端路由的。
使用到的api:
window.location.hash = 'qq' // 设置 url 的 hash,会在当前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function(){
// 监听hash变化,点击浏览器的前进后退会触发
})
history模式
已经有 hash 模式了,而且 hash 能兼容到IE8, history 只能兼容到 IE10,为什么还要搞个 history 呢?
首先,hash 本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。其次,hash 的传参是基于 url 的,如果要传递复杂的数据,会有体积的限制,而 history 模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
最重要的一点:
如果不想要很丑的 hash,我们可以用路由的 history 模式
—— 引用自 vueRouter文档
相关API:
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/
window.history.replaceState(state, title, url)
// 与 pushState 基本相同,但她是修改当前历史记录,而 pushState 是创建新的历史记录
window.addEventListener("popstate", function() {
// 监听浏览器前进后退事件,pushState 与 replaceState 方法不会触发
});
window.history.back() // 后退
window.history.forward() // 前进
window.history.go(1) // 前进一步,-2为后退两步,window.history.lengthk可以查看当前历史堆栈中页面的数量
history 模式改变 url 的方式会导致浏览器向服务器发送请求,这不是我们想看到的,我们需要在服务器端做处理:如果匹配不到任何静态资源,则应该始终返回同一个 html 页面。
HTML5 history
新增了两个API
:history.pushState
和history.replaceState
两个API
都接收三个参数:
-
状态对象(
state object
):一个JavaScript对象,与用pushState()
方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate
事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。 -
标题(
title
):FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。 -
地址(
URL
): 新的历史记录条目的地址。浏览器不会在调用pushState()
方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()
会抛出异常。该参数是可选的;不指定的话则为文档当前URL。
相同之处是两个API
都会操作浏览器的历史记录,而不会引起页面的刷新。
不同之处在于pushState
会增加一条新的历史记录,而replaceState
则会替换当前的历史记录。
这里大家可以先F12
试试,看看地址栏发生了什么变化
1 2 3 4 5 6 7 | window.history.pushState(null, null, "hell"); window.history.pushState(null, null, "/hell"); window.history.pushState(null, null, "#/hello"); window.history.pushState(null, null, "?name="); |
注意:这里的url不支持跨域,否则会抛出异常
尝试
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>前端路由实现</title> <style> .warp{ width:400px; height:400px; border:1px solid grey; margin:0 auto; } .nav{ border-bottom:1px solid grey; } .nav li{ display:inline-block; list-style:none; } .nav li a{ display:inline-block; text-decoration: none; padding:10px 15px; } .router{ padding:20px; } a{ cursor: pointer; } </style> </head> <body> <section class="warp"> <div class="nav"> <ul> <li><a href="javascript:void(0)" data-path="index">首页</a></li> <li><a href="javascript:void(0)" data-path="news">新闻</a></li> <li><a href="javascript:void(0)" data-path="about">关于</a></li> </ul> </div> <div id="router" class="router"> <!-- 内容加载区域 --> </div> </section> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script src="./router.js"></script> </body> </html> |
一:简单开胃菜
router.js
1 2 3 4 5 6 7 8 9 10 | ;(function(){ history.replaceState(null,null,'');//最开始的状态,采用replace直接替换 $('#router').html('<p>nav1</p>') $('a').on('click',function(){ console.log(this.text) var text = this.text; $('#router').html('<p>'+ text +'</p>') history.pushState(null,null,'#/'+text); }) })() |
最简单的示例,只能监听点击事件,而浏览器中的后、前进都不能监听地址栏的改变
二、数据状态管理
router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 状态版 ;(function(){ var count = [0,0,0] $('#router').html('<p>导航1:</p>'+count[0]+'<p>导航2:</p>'+count[1]+'<p>导航3:</p>'+count[2]) history.replaceState(count,null,'');//最开始的状态,采用replace直接替换 for(var i = 0 ; i<$('a').length; i++){ $('a')[i].index = i $('a').eq(i).on('click',function(){ console.log(this.index); var index = this.index; count[index]++; $('#router').html('<p>导航1:</p>'+count[0]+'<p>导航2:</p>'+count[1]+'<p>导航3:</p>'+count[2]) history.pushState(count,null,'#/count'+count[index]);//之后的状态,需要进行保存 }) } //监听history其他api导致地址栏url改变事件 window.addEventListener('popstate',function(e){ console.log(e.state); var state = e.state; $('#router').html('<p>导航1:</p>'+state[0]+'<p>导航2:</p>'+state[1]+'<p>导航3:</p>'+state[2]) }) })() |
popstate
当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。
需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back())
三:回归简单
router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ;(function(){ var url = 'nav1'; history.replaceState(url,null,'');//最开始的状态,采用replace直接替换 $('#router').html('<p>'+url+'</p>') $('a').on('click',function(){ console.log(this.text) url = this.text; $('#router').html('<p>'+ url +'</p>') history.pushState(url,null,'#/'+url); }) window.addEventListener('popstate',function(e){ console.log(e.state); url = e.state $('#router').html('<p>'+ url +'</p>') }); })() |
兜兜转转我们算是回到了起点,但是通过这张图我们会发现页面点击刷新按钮会有导航和内容块不一致的内容,所以我们需要改进他,并且监听load
事件
改进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ;(function(){ $('a').on('click',function(){ console.log(this.text) url = this.text; $('#router').html('<p>'+ url +'</p>') history.pushState(url,null,'#/'+url); }) window.addEventListener('popstate',function(e){ console.log(e.state); url = e.state $('#router').html('<p>'+ url +'</p>') }); window.addEventListener('load',function(){ url = location.hash.slice(2) || 'nav1'; history.replaceState(url,null,''); console.log(location.hash); $('#router').html('<p>'+ url +'</p>'); }); })() |
可以看到我们点击刷新的时候导航和内容区域一致了。
四:路由页面引进
我们这里还是采用了ajax
的load
方法
router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | ;(function(){ var router = [ { 'path':'index', 'url':'./main.html' }, { 'path':'news', 'url':'./news.html' }, { 'path':'about', 'url':'./about.html' } ]; //改变页面 function display_page(url){ $('#router').load(url) } $('a').on('click',function(){ var path = $(this).data('path'); console.log(path) for(var i in router){ if(router[i].path == path){ display_page(router[i].url); history.pushState(router[i].url,null,router[i].path); } } }) window.addEventListener('popstate',function(e){ var url = e.state; display_page(url); }); window.addEventListener('load',function(){ var start = location.href.lastIndexOf('/'); var path = location.hash.slice(start) || 'index'; console.log(path) for(var i in router){//刷新 加载 console.log(1) if(router[i].path == path){ display_page(router[i].url); history.replaceState(router[i].url,null,path); break; } if(i == router.length-1){//重定向 display_page(router[0].url); history.replaceState(router[i].url,null,router[0].path); } } }); })() |
可以看到基本是实现了history
路由功能,但是这里有一个问题就是刷新后因为地址栏url原因会报错,也就是找不到这个页面,这是由于刷新的时候是重载,重新向网站目录查找文件,而我们当前目录并没有这个文件资源所以导致报错。需要后台拦截! 放弃!
折中
最后我还是屈服于#
了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | ;(function(){ var router = [ { 'path':'index', 'url':'./main.html' }, { 'path':'news', 'url':'./news.html' }, { 'path':'about', 'url':'./about.html' } ]; //改变页面 function display_page(url){ $('#router').load(url) } $('a').on('click',function(){ var path = $(this).data('path'); console.log(path) for(var i in router){ if(router[i].path == path){ display_page(router[i].url); history.pushState(router[i].url,null,'#/'+router[i].path); } } }) window.addEventListener('popstate',function(e){ var url = e.state; display_page(url); }); window.addEventListener('load',function(){ var path = location.hash.slice(2) || '/index'; console.log(path) for(var i in router){//刷新 加载 console.log(1) if(router[i].path == path){ display_page(router[i].url); history.replaceState(router[i].url,null,'#/' + path); break; } if(i == router.length-1){//重定向 display_page(router[0].url); history.replaceState(router[0].url,null,'#/' + router[0].path); } } }); })(); |