[高级]深入浅出history对象

一、history简介

History 对象包含用户(在浏览器窗口中)访问过的 URL,它是 window 对象的一部分,可通过 window.history 属性对其进行访问。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,可以看到history不为空对象。

结论

window.history对象是不可变的

问题2

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

探索
探索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.length

接下来我们使用location.href = 'https://www.zhihu.com'来进行跳转,发现页面跳转到了知乎,此时我们再打印一下history,发现length变为了3。
百度h5页面

打印history.length

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

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

百度h5页面

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

跳转后效果

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

结论

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

问题4

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

探索

百度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.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')来进入前端板块,看看会发生什么。
pushState以后的页面

可以看到,此时url已经变了,但是页面并没有渲染出前端模块。
vue-router-push函数

我们顺势来看一看vue-router的源码,我们可以看到它调用了一个pushState函数,我们来看看这个函数
vue-router-pushState函数

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


前端路由demo
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>前端路由实现</title>
  <style>
    .link {
      color: #999;
      cursor: pointer;
    }
    .link:hover {
      text-decoration: underline;
    }
  </style>
</head>
<body>
<ul>
  <li><a class="link" data-href="/A">A</a></li>
  <li><a class="link" data-href="/B">B</a></li>
  <li><a class="link" data-href="/C">C</a></li>
  <li><a class="link" data-href="/D">D</a></li>
</ul>

<div id="wrapper"></div>


<script>
  // 创建实例
  const router = new Router();
  const contentDOM = document.querySelector('#wrapper');
  // 注册路由
  router.route('/A', state => {
    contentDOM.innerHTML = 'A';
  });
  router.route('/B', state => {
    contentDOM.innerHTML = 'B';
  });
  router.route('/C', state => {
    contentDOM.innerHTML = 'C';
  });
  router.route('/D', state => {
    contentDOM.innerHTML = 'D';
  });
</script>
</body>
</html>

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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>前端路由实现</title>
  <style>
    .link {
      color: #999;
      cursor: pointer;
    }
    .link:hover {
      text-decoration: underline;
    }
  </style>
  <script>

    const noop = () => undefined;

    class Router {
      constructor() {
        this.init();
      }

      // 初始化
      init() {
        this.routes = {};
        this.doListen();
        this.makeLink();
      }
      // 监听
      doListen() {
        window.addEventListener('DOMContentLoaded', this.listenEventInstance.bind(this));
        window.addEventListener('popstate', this.listenEventInstance.bind(this));
      }

      // 监听事件后,触发路由的回调
      listenEventInstance() {
        this.callbackCenter(window.location.pathname);
      };

      // 注册路由,将回调函数存储下来
      route(pathname, callback = noop) {
        this.routes[pathname] = callback;
      }

      // 回调
      callbackCenter(pathname) {
        if (!this.routes[pathname]) {
          return;
        }
        const {state} = window.history;
        this.routes[pathname](state);
      }

      // 绑定 a 标签,阻止默认行为
      makeLink() {
        document.addEventListener('click', e => {
          const {target} = e;
          const {nodeName, dataset: {href}} = target;
          if (!(nodeName === 'A') || !href) {
            return;
          }
          e.preventDefault();
          window.history.pushState(null, '', href);
          this.callbackCenter(href);
        });
      }
    }

  </script>
</head>
<body>
<ul>
  <li><a class="link" data-href="/A">A</a></li>
  <li><a class="link" data-href="/B">B</a></li>
  <li><a class="link" data-href="/C">C</a></li>
  <li><a class="link" data-href="/D">D</a></li>
</ul>

<div id="wrapper"></div>


<script>
  // 创建实例
  const router = new Router();
  const contentDOM = document.querySelector('#wrapper');
  // 注册路由
  router.route('/A', state => {
    contentDOM.innerHTML = 'A';
  });
  router.route('/B', state => {
    contentDOM.innerHTML = 'B';
  });
  router.route('/C', state => {
    contentDOM.innerHTML = 'C';
  });
  router.route('/D', state => {
    contentDOM.innerHTML = 'D';
  });
</script>
</body>
</html>

五、总结

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

六、参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,702评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,143评论 3 415
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,553评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,620评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,416评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,940评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,024评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,170评论 0 287
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,709评论 1 333
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,597评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,784评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,291评论 5 357
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,029评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,407评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,663评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,403评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,746评论 2 370

推荐阅读更多精彩内容

  • 浏览器窗口有一个history对象,用来保存浏览历史。 如果当前窗口先后访问了三个网址,那么history对象就包...
    君临12138阅读 3,081评论 0 3
  • 概述 浏览器窗口有一个history对象,用来保存浏览历史。 如果当前窗口先后访问了三个网址,那么history对...
    许先生__阅读 754评论 0 2
  • 概述 window.history属性指向History对象,它表示当前窗口的浏览历史。 History对象保存了...
    oWSQo阅读 9,813评论 0 3
  • 1.概述 window.history属性指向 History 对象,它表示当前窗口的浏览历史。 History ...
    coffee1949阅读 582评论 0 0
  • History 对象包含用户(在浏览器窗口中)访问过的 URL。 history对象属性 length: 返回历史...
    广告位招租阅读 1,477评论 0 1