一、history简介
History 对象包含用户(在浏览器窗口中)访问过的 URL,它是 window 对象的一部分,可通过 window.history 属性对其进行访问。history对象在前端应用中至关重要,所有单页应用的路由都是基于history对象。
二、导读
本文会先简单介绍history对象的一些属性,然后会重点介绍history对象的一些实际应用,以此来帮助我们加深对history对象的理解。
三、属性介绍
上图是我在控制台打印的history对象,下面我们简单介绍一下这些属性。
3.1 属性值
- length:返回浏览器历史列表中的 URL 数量。
- scrollRestoration: 滚动恢复属性允许web应用程序在历史导航上显式地设置默认滚动恢复行为。该属性有两个可选值,默认为auto,将恢复用户已滚动到的页面上的位置。另一个值为:manual,不还原页上的位置,用户必须手动滚动到该位置。
- state:返回一个表示历史堆栈顶部的状态的值,这是一种可以不必等待popstate事件而查看状态的方式。
3.2 方法
- history.pushState(object, title, url)方法接受三个参数,object 为随着状态保存的一个对象,title为新页面的标题,url为新的网址。
- replaceState(object, title, url) 与pushState的唯一区别在于该方法是替换掉history栈顶元素。
- history.go(x) 去到对应的url历史记录。
- history.back() 相当于浏览器的后退按钮。
- history.forward() 相当于浏览器的前进按钮。
3.3 事件
- popstate事件:popstate事件会在以下的情况触发:
同一个文档的浏览历史发生变化时触发。调用history.pushState()和history.replaceState()方法不会触发。而用户点击浏览器的前进/后退按钮时会触发,调用history对象的back()、forward()、go()方法时,也会触发。popstate事件的回调函数的参数为event对象,该对象的state属性为随状态保存的那个对象。
3.4 理解
3.4.1问题
介绍了history对象,我们先抛出几个小问题:
1.history对象可变吗?
2.history.length既然代表浏览器历史列表中的URL数量,那么这个数量可以无限多吗?
3.location.href与history.pushState有什么区别?
4.如果我从A域名跳转到了B域名,那么history.back()会回到哪里?
5.popstate事件的触发条件是什么?
3.4.2 解答
下面我们来依次解答这几个问题,初步加深对history对象的理解。
问题1
history对象可变吗?
探索
我们给history赋值为空对象,然后打印一下history,可以看到history不为空对象。
结论
window.history对象是不可变的
问题2
history.length既然代表浏览器历史列表中的URL数量,那么这个数量可以无限多吗?
探索
我们首先打印出history.length,发现结果为3;然后我们添加100条记录,再次打印history.length,发现值为50。
结论
history.length并不会无限大
问题3
location.href与history.pushState有什么区别?
探索
[图片上传中...(image.png-a52ee3-1609847856284-0)]
我们以百度h5页面来举例,首先我们进入http:www.baidu.com,同时打印一下history对象,length为2。
接下来我们使用location.href = 'https://www.zhihu.com'来进行跳转,发现页面跳转到了知乎,此时我们再打印一下history,发现length变为了3。
此时我们点击浏览器的返回,再次回到百度h5页面,打印一下history,依然为3。
此时我们使用history.pushState(null, ' ', https://www.zhihu.com'),发现抛出一个错误,意思就是pushState是不能用来在不同域名之间跳转的。
接下来我们使用history.pushState(null, ' ', /a'),发现页面的url后面添加了一个'/a'路径,但是观察控制台,发现并没有往服务器再发送任何请求。
我们再使用一下location.href = '/a',发现浏览器再次发起了文档请求,页面变为了Not Found
结论
1.使用location.href跳转后页面会发起新的文档请求,而history.pushState不会。
2.location.href可以跳转到其他域名,而history不能。
3.location.href与history都会往历史列表中添加一条记录。
问题4
如果我从A域名跳转到了B域名,那么history.back()会回到哪里?
探索
还是以百度h5页面为例
我们使用location.href = 'https://www.zhihu.com'进行跳转
接着,使用history.back()方法,页面又回到了www.baidu.com页面
结论
从A域名跳转到了B域名,那么调用history.back()会回到A域名
问题5
popstate事件的触发条件是什么?
探索
首先我们监听一下popstate事件,然后我依次调用了location.href,location.hash,history.go,history.back,history.forward,history.pushState,history.replaceState方法,得出结果如下
结论
1.因为location.href是刷新式的跳转,所以这个打印信息是肯定打印不出来的,在刷新的时候这个监听函数就已经失效了,所以这里不讨论location.href会不会触发popstate事件。跟location.href类似的还有history.go(0),因为history.go(0)也会直接刷新页面,所以这个监听函数也会失效,也不会打印出信息。
2.location.hash是会触发popstate事件的,同样会触发popstate的还有history.back,history.forward,history.go。
3.history.pushState,history.replaceState都不会触发popstate事件。
四、应用
通过以上几个问题,我们初步了解了history对象,下面我们来看一下它的一些实际应用
4.1 单页应用
history最常见的使用就是搭建前端单页应用
使用history.pushState方法可以改变地址栏的路径而不用刷新页面,所以这使得我们只需要在第一次进入页面的时候去请求一次html,后续的页面呈现则交由js来控制,根据不同url路径来加载不同的js模块。
使用history路由需要注意的是服务器需要做好处理 URL 的准备,因为当用户在url为'/a/b/c'的页面进行刷新操作,服务器很有可能会因为匹配不到路径而返回404状态码,应当对这样的路径也都返回html文件。
4.2 交互操作
问题
另一类比较常见的,就是一些交互实现类。比如说以下交互:
1.在创建/编辑页面,用户修改了表单以后,如果退出的时候,给出二次弹窗确认。
2.在移动端的列表页,点击筛选框会弹出一个浮层,当用户点击app的后退按钮时,把浮层关闭掉,而不是回退页面。
3.当前处在页面A,点击跳转到页面B,由页面B内请求发现当前用户无权限,于是跳转到错误页C,如果避免用户在C页面点击浏览器的回退按钮再次回到B页面。
解答
分析
1.交互1与交互2是同一类问题,原理都是点击浏览器的前进与后退按钮都会触发popstate事件,监听这个popstate事件,一旦触发,便给出一个弹窗。需要注意的是,当popstate事件触发的时候,历史地址记录就已经被回退了,我们无法阻止这个回退,所以在回退之前,我们需要使用history.pushState(null,null,document.URL)方法去主动再添加一条当前url的记录,当popstate事件触发的时候,虽然回退了一条记录,但是url并不会改变,也就达到了停留在当前页面的目的。
2.关于交互3,我们要学会使用history.replace方法,如果我们一直使用pushState或者location.href进行跳转的话,那么此时历史记录是这样的A—B—C,但是如果我们从B到C跳转的时候使用history.replace的话,B记录就会被替换为C记录,那么历史记录就会变为A—C,此时从C页面点击返回按钮就可以直接返回A页面。
实例
下面我给出一个点击浏览器的后按钮后弹窗的效果,供大家参考。
还是以百度h5页面举例,在'/a'页面,我点击返回的时候,会弹出禁止返回的弹窗。
具体代码如下,可在控制台使用
history.pushState(null, null, '/a')
window.addEventListener('popstate', () => {
alert('禁止返回')
})
history.pushState(null, null, document.URL)
4.3 各种路由框架的基础
路由框架通常都有三种模式:browserHistory,hashHistory,memoryHistory,其中browserHistory的实现就是依赖于window.history对象,下面我们先来想两个问题,然后接着来实现一个简单的前端单页路由。
问题
1.用window.history.pushState和路由框架的pushState有什么区别?
2.既然使用history.pushState无法触发popstate事件,那么路由框架又是如何在pushState的时候加载不同组件的呢?
3.为什么使用pushState跳转以后,history对象的state里都有一个属性key?
解答
下面咱们来分析一下这几个问题。
实验
首先我们掘金的首页,点击前端板块,发现在进入'/frontend'路径时,并没有发送html请求,说明这是一个单页应用,下面我们再返回首页,使用history.pushState(null, null, '/frontend')来进入前端板块,看看会发生什么。
可以看到,此时url已经变了,但是页面并没有渲染出前端模块。
我们顺势来看一看vue-router的源码,我们可以看到它调用了一个pushState函数,我们来看看这个函数
并没有看出什么特别的地方,这儿的pushState就是调用了history.pushState函数。不过从这里我们看出了问题3的答案,vue-router在使用push函数的时候调用了history.pushState方法,而这里在使用history.pushState函数时往里面加了一个key。
我们可以看到这个key的值就是一个时间,有什么特殊含义吗?后来查阅官方文档,得出了这样的解释:
当一个 history 通过应用程序的 push 或 replace 跳转时,它可以在新的 location 中存储 “location state” 而不显示在 URL 中,这就像是在一个 HTML 中 post 的表单数据。 在 DOM API 中,这些 hash history 通过 window.location.hash = newHash 很简单地被用于跳转,且不用存储它们的location state。但我们想全部的 history 都能够使用location state,因此我们要为每一个 location 创建一个唯一的 key,并把它们的状态存储在 session storage 中。当访客点击“后退”和“前进”时,我们就会有一个机制去恢复这些 location state。
我们再回到之前的问题一与问题二,既然这个pushState没有什么特别的,我们再来看一看这个transitionTo函数。
我发现了这段代码,这里调用了该路由的回调函数。众所周知,我们注册一个路由一般是采用这种形式
router.route('/111', state => { contentDOM.innerHTML = '111';});
这里就是执行了state => { contentDOM.innerHTML = '111'; }
这个回调函数,所以问题就清楚了,路由框架的pushState不仅调用了history.pushState方法,还调用了该路由对应的回调函数来渲染了对应的组件。
结论
所以我们得出结论,路由框架的pushState与history.pushState是不一样的,路由框架的pushState不仅调用了history.pushState改变了url,更重要的是它还多了一步操作,即根据这个url销毁了旧组件,渲染了新组件;至于state里面的key值,则是为了兼容hashHistory。
前端路由demo
下面我们来实现一个前端路由的demo,现在已经有一个html,我们需要为它写一个Router,实现如下效果:
前端路由实现
简单分析一下:
1.首先发布订阅模式肯定少不了,注册路由的时候,需要将每个路由所对应的回调函数存储起来,在路由变化的时候执行对应的回调函数。
2.只监听popSate是不够的,页面初始化的时候,以及pushState的时候,都需要执行对应的回调函数去主动更新一下组件。
3.还有一个问题,就是需要阻止这几个a标签的默认事件。
经过以上对history的理解,这个简单的Router已经不难实现了,下面直接给出完整代码:
前端路由实现
五、总结
本文首先介绍了history对象的各个属性,然后介绍了它的一些应用,希望本文能在实际工作中对大家有所帮助。在前端路由这块儿除了window.history以外,其他知识点以及相关应用还有很多。对于location对象、搭建多页应用等其他知识,大家感兴趣的话可以去深入探究。
六、参考
- jqhtml.com: 单页应用的部署方案
- 掘金: 性能 & 集成 —— History API
- react-router: react-router文档
- vue: vue源码
- MDN: history对象