[高级]深入浅出history对象

一、history简介

History 对象包含用户(在浏览器窗口中)访问过的 URL,它是 window 对象的一部分,可通过 window.history 属性对其进行访问。history对象在前端应用中至关重要,所有单页应用的路由都是基于history对象。

二、导读

本文会先简单介绍history对象的一些属性,然后会重点介绍history对象的一些实际应用,以此来帮助我们加深对history对象的理解。

三、属性介绍

[高级]深入浅出history对象_第1张图片
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对象_第2张图片
给history赋值

我们给history赋值为空对象,然后打印一下history,可以看到history不为空对象。

结论

window.history对象是不可变的

问题2

history.length既然代表浏览器历史列表中的URL数量,那么这个数量可以无限多吗?

探索
[高级]深入浅出history对象_第3张图片
探索history.length

我们首先打印出history.length,发现结果为3;然后我们添加100条记录,再次打印history.length,发现值为50。

结论

history.length并不会无限大

问题3

location.href与history.pushState有什么区别?

探索

[图片上传中...(image.png-a52ee3-1609847856284-0)]

打印history.length

我们以百度h5页面来举例,首先我们进入http:www.baidu.com,同时打印一下history对象,length为2。
[高级]深入浅出history对象_第4张图片
知乎页面

打印history.length

接下来我们使用location.href = 'https://www.zhihu.com'来进行跳转,发现页面跳转到了知乎,此时我们再打印一下history,发现length变为了3。
[高级]深入浅出history对象_第5张图片
百度h5页面

打印history.length

此时我们点击浏览器的返回,再次回到百度h5页面,打印一下history,依然为3。
pushState跳转其他域名

此时我们使用history.pushState(null, ' ', https://www.zhihu.com'),发现抛出一个错误,意思就是pushState是不能用来在不同域名之间跳转的。
pushState跳转当前域名

[高级]深入浅出history对象_第6张图片
百度h5页面

接下来我们使用history.pushState(null, ' ', /a'),发现页面的url后面添加了一个'/a'路径,但是观察控制台,发现并没有往服务器再发送任何请求。
location.href跳转

[高级]深入浅出history对象_第7张图片
跳转后效果

我们再使用一下location.href = '/a',发现浏览器再次发起了文档请求,页面变为了Not Found

结论

1.使用location.href跳转后页面会发起新的文档请求,而history.pushState不会。
2.location.href可以跳转到其他域名,而history不能。
3.location.href与history都会往历史列表中添加一条记录。

问题4

如果我从A域名跳转到了B域名,那么history.back()会回到哪里?

探索

[高级]深入浅出history对象_第8张图片
百度h5页面

还是以百度h5页面为例
location.href跳转知乎

我们使用location.href = 'https://www.zhihu.com'进行跳转
history.back回退

百度页面

接着,使用history.back()方法,页面又回到了www.baidu.com页面

结论

从A域名跳转到了B域名,那么调用history.back()会回到A域名

问题5

popstate事件的触发条件是什么?

探索
监听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对象_第9张图片
弹窗提示

具体代码如下,可在控制台使用

   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?

解答

下面咱们来分析一下这几个问题。

实验

[高级]深入浅出history对象_第10张图片
掘金前端板块

首先我们掘金的首页,点击前端板块,发现在进入'/frontend'路径时,并没有发送html请求,说明这是一个单页应用,下面我们再返回首页,使用history.pushState(null, null, '/frontend')来进入前端板块,看看会发生什么。
[高级]深入浅出history对象_第11张图片
pushState以后的页面

可以看到,此时url已经变了,但是页面并没有渲染出前端模块。
[高级]深入浅出history对象_第12张图片
vue-router-push函数

我们顺势来看一看vue-router的源码,我们可以看到它调用了一个pushState函数,我们来看看这个函数
[高级]深入浅出history对象_第13张图片
vue-router-pushState函数

并没有看出什么特别的地方,这儿的pushState就是调用了history.pushState函数。不过从这里我们看出了问题3的答案,vue-router在使用push函数的时候调用了history.pushState方法,而这里在使用history.pushState函数时往里面加了一个key。
[高级]深入浅出history对象_第14张图片
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函数。
[高级]深入浅出history对象_第15张图片
vue-router-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,实现如下效果:


[高级]深入浅出history对象_第16张图片
前端路由demo



  
  前端路由实现
  




简单分析一下:
1.首先发布订阅模式肯定少不了,注册路由的时候,需要将每个路由所对应的回调函数存储起来,在路由变化的时候执行对应的回调函数。
2.只监听popSate是不够的,页面初始化的时候,以及pushState的时候,都需要执行对应的回调函数去主动更新一下组件。
3.还有一个问题,就是需要阻止这几个a标签的默认事件。
经过以上对history的理解,这个简单的Router已经不难实现了,下面直接给出完整代码:




  
  前端路由实现
  
  




五、总结

本文首先介绍了history对象的各个属性,然后介绍了它的一些应用,希望本文能在实际工作中对大家有所帮助。在前端路由这块儿除了window.history以外,其他知识点以及相关应用还有很多。对于location对象、搭建多页应用等其他知识,大家感兴趣的话可以去深入探究。

六、参考

  • jqhtml.com: 单页应用的部署方案
  • 掘金: 性能 & 集成 —— History API
  • react-router: react-router文档
  • vue: vue源码
  • MDN: history对象

你可能感兴趣的:([高级]深入浅出history对象)