阅读 370

[高级]深入浅出浏览器的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 = 'www.zhihu.com'来进行跳转,发现页面跳转到了知乎,此时我们再打印一下history,发现length变为了3。 百度h5页面 打印history.length 此时我们点击浏览器的返回,再次回到百度h5页面,打印一下history,依然为3。 pushState跳转其他域名 此时我们使用history.pushState(null, ' ', 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 = '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。

4.4 前端路由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对象、搭建多页应用等其他知识,大家感兴趣的话可以去深入探究。

六、参考

文章分类
前端
文章标签