理解浏览器历史记录(2)- hashchange、pushState

本文也是一篇基础文章。继上文之后,本打算去研究pushState,偶然在一些信息中发现了锚点变化对浏览器的历史记录也会影响,同时锚点的变化跟pushState也有一些关联。所以就花了点时间,把这两个东西尽量都琢磨清楚。本文记录相关的一些要点及研究过程。

1. hashchange

这个部分的内容也已经补充到上文的最后了,这里只是细化一下。总的结论是:如果一个网页只是锚点,也就是location.hash发生变化,也会导致历史记录栈的变化;且变化相关的所有特性,都与上文描述的整个页面变化的特性相同。常见的改变网页锚点的方式有:

1)直接更改浏览器地址,在最后面增加或改变#hash;

2)通过改变location.href或location.hash的值;

3)通过触发点击带锚点的链接;

4)浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。

假如我们还用上文的demo(http://liuyunzhuge.github.io/blog/history/demo1.html)来测试,并按照以下步骤操作的话:

打开新选项卡;输入demo1.html;在地址栏后面加#1;将地址栏#1改成#2;将地址栏#2改成#3;将地址栏#3改成#1。

那么历史记录栈的存储状态就应该类似下面这个形式:

由于锚点变化也会在历史记录栈添加新的记录,所以history.length也会在锚点变化之后改变。每当锚点发生变化的时候,主流浏览器还会触发window对象的onhashchange事件,在这个事件回调里面,我们通过事件对象和location能够拿到很有用三个参数:

window.onhashchange = function(event) {

console.log(event.oldURL);

console.log(event.newURL);

console.log(location.hash);

};

event.oldURL返回锚点变化前的完整浏览器地址;

event.newURL返回锚点变化后的完整浏览器地址;

location.hash返回锚点变化后页面地址中的锚点值。

借助于这三个信息,可以在hashchange回调内加一些控制器的逻辑,来实现单页程序开发里面关键的路由功能。现简单实现举例如下:

<!doctype html>

<html lang="en">

<head>

<meta charset="UTF-8">

<title>Document</title>

<link rel="stylesheet" href="./css/quick_layout.css"/>

<script src="./js/jquery.js"></script>

<script src="./js/demo.js"></script>

<style type="text/css">

ul {

list-style: none;

}

* {

padding: 0;

margin: 0;

}

.menu {

width: 320px;

margin: 10px auto;

text-align: center;

}

.menu li,

.menu a {

float: left;

width: 100px;

}

.menu > .active > a {

font-weight: bold;

}

.menu > li + li {

margin-left: 10px;

}

</style>

</head>

<body>

<div id="container" class="container"></div>

<script>

//容器

var Container = {

$element: $(‘#container‘),

actions: {}

};

//action实例配置定义

var Actions = {

‘index‘: {

destroy: function () {

this.$content.remove();

},

doAction: function () {

var $content = this.$content = $(‘<div class="content">这是首页的内容</div>‘);

$content.appendTo(Container.$element);

}

},

‘list‘: {

destroy: function () {

this.$content.remove();

},

doAction: function () {

var $content = this.$content = $(‘<div class="content">这是列表页的内容</div>‘);

$content.appendTo(Container.$element);

}

},

‘about‘: {

destroy: function () {

this.$content.remove();

},

doAction: function () {

var $content = this.$content = $(‘<div class="content">这是关于页的内容</div>‘);

$content.appendTo(Container.$element);

}

}

};

//公共方法,渲染菜单

var getMenu = function (actionName) {

return [‘<ul class="menu fix">‘,

‘        <li class="‘ + (actionName == ‘index‘ ? ‘active‘ : ‘‘) + ‘"><a href="#index">首页</a></li>‘,

‘        <li class="‘ + (actionName == ‘list‘ ? ‘active‘ : ‘‘) + ‘"><a href="#list">列表页</a></li>‘,

‘        <li class="‘ + (actionName == ‘about‘ ? ‘active‘ : ‘‘) + ‘"><a href="#about">关于页</a></li>‘,

‘    </ul>‘].join("");

};

function hashchange(event) {

var actionName = (location.hash || ‘#index‘).substring(1);

//重复

if (Container._current && Container._current.actionName == actionName) {

return;

}

//未定义

if (!Actions[actionName]) {

return;

}

//已定义的action

var action = Container.actions[actionName];

//销毁之前的action

Container._current && Container._current.destroy();

if (!action) {

//未定义则立即创建

action = (function () {

//action实例

var ret = $.extend(true, {

destory: $.noop,

doAction: $.noop

}, Actions[actionName]);

//添加actionName属性

ret.actionName = actionName;

//代理destroy方法,封装公共逻辑

ret.destroy = (function () {

var _destroy = ret.destroy;

return function () {

//移除菜单

ret.$menu.remove();

//调用Actions中定义的destroy方法

_destroy.apply(ret, arguments);

};

})();

//代理doAction方法,封装公共逻辑

ret.doAction = (function () {

var _doAction = ret.doAction;

return function () {

//添加菜单

var $menu = ret.$menu = $(getMenu(ret.actionName));

$menu.appendTo(Container.$element);

//调用Actions中定义的doAction方法

_doAction.apply(ret, arguments);

}

})();

return ret;

})();

}

Container._current = action;

action.doAction();

}

//初始化调用

hashchange();

//用hashchange当页面切换的控制器

window.onhashchange = hashchange;

</script>

</body>

</html>

本代码demo可通过以下地址访问测试:

http://liuyunzhuge.github.io/blog/pushState/demo1.html。这个demo中,浏览器前进后退,页面刷新,链接跳转,都能保证内容正确显示。当然这只是一个极为简单的举例,真正的SPA的路由功能远比此复杂,下一步我会花时间研究一个较为流行的路由实现,到时再写文来总结单页路由的实现思路。

window.onhashchange的mdn参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange

以上是我了解到hashchange的绝大部分用得着的内容,下面要介绍的pushState,还会有一点跟它相关的东西。在SPA的路由实现中,hashchange与pushState是搭配在一起使用的,所以在真正了解路由实现前,把这2个东西的基础知识了解透彻也是非常有必要的。

2 . pushState

有了之前对历史记录栈的认识,再来了解pushState就会比较容易。pushState相关的内容包含三个东西:2个api和一个事件。2个api分别是history.pushState和history.replaceState,1个事件是指window.onpopstate事件。pushState提供给我们的是一种在不改变网页内容的前提下,操作浏览器历史记录的能力。

下面详细看看这2个api和1个事件的内容:

1)history.pushState(stateObj,title,url)

这个方法用来在浏览器历史记录栈中当前指针后面压入一条新的条目,然后将当前指针移到这条最新的条目;如果在压入新条目的时候,当前指针的后面还有旧的条目,在压入新的之后也会被废弃掉。整体特性其实跟上一篇博客介绍的,在同一个窗口打开另外一个页面对历史记录栈的作用完全相似,只不过history.pushState仅仅是添加新的条目,并且激活它,然后改变浏览器的地址,但是不会改变网页内容,它也不会去验证这个新条目对应的网页是否存在。

这个api有三个参数,第二个参数目前浏览器都是忽略它的,在使用的时候一般传入空字符串即可;第三个参数对应的是新条目的地址,如果没有,默认就是当前文档的地址;第一个参数是一个object对象,它会与新条目绑定在一起,可以用来存储一些简单的数据,不过不能存太多,firefox对它的限制是640K,这个对象可以通过onpopstate事件对象的state属性来访问。

为了验证前面这部分的理论,可以通过这个demo:http://liuyunzhuge.github.io/blog/pushState/demo2.html,按以下步骤做一些操作测试:

打开新选项卡;输入该demo地址;点击demo3的链接;点击demo4的链接;点击demo4里的返回;点击demo3里的返回;点击pushState(‘foo’)的按钮;点击pushState(‘bar’)的按钮。

浏览器历史记录栈的变化过程应该是下面这个状态:

2)history.replaceState(stateObj,title,url)

这个api和history.pushState的用法完全一致,只不过它不会在历史记录栈中增加新的条目,只会影响当前条目,比如如果传递了stateObj,就会更新当前条目关联的状态对象;如果传递了url,就会替换当前条目的页面地址和更改浏览器地址栏的地址。有一种非常常见的场景,如果利用replaceState,可以优化它的实现方式。

网页中搜索列表是比较常见的功能:

有2种常见的方式来实现这样的功能:

一是将查询条件区封装好,列表展示区封装好,当查询条件改变的时候,利用ajax,触发列表的查询;但是这种方式有个不好的体验问题就是,查询条件更改后,如果刷新页面,查询条件不能恢复刷新前的状态;所以就有了第二种方式;

二是在查询条件更改的时候,不用ajax更换列表,而是更新url参数,重新刷新页面,然后在后端或在前端将查询条件的状态根据url里面的参数初始化好再展示。

目前电商都是第二种方式多,一来比较简单,二来兼容性也好。如果不考虑兼容IE9以前的浏览器,利用replaceState可以优化第一种做法:就是在查询条件更改的时候,除了用ajax查询数据,同时用replaceState更新页面的url,把条件封装到url参数中;当用户刷新页面时,根据url里面的条件参数做查询条件的初始化,这一步跟第二个方案的做法一致。

history.pushState和history.replaceState还有一个共同的特点就是都不会触发hashchange,你可以下面这个demo来测试:http://liuyunzhuge.github.io/blog/pushState/demo5.html,以新选项卡打开这个demo,不管先点击什么按钮,页面上都不会看到有任何的打印信息,尽管我在代码中是有添加window.onhashchange回调的:

但是当我直接在地址栏后面添加一个#3的时候,页面上就会看到onhashchange回调打印的信息了:

3) window.onpopstate事件

这个事件触发的时机比较有特点:

一、history.pushState和history.replaceState都不会触发这个事件

二、仅在浏览器前进后退操作、history.go/back/forward调用、hashchange的时候触发

你可以下面这个demo来验证:

http://liuyunzhuge.github.io/blog/pushState/demo6.html,这个demo里我添加了onpopstate回调,尝试打印一些信息,如果按以下几组步骤测试:

a. 打开新选项卡,输入demo地址,点击pushState的按钮,再点击浏览器的后退按钮,再点击浏览器前进按钮;

b. 打开新选项卡,输入demo地址,点击pushState的按钮,点击replaceState的按钮,再点击浏览器的后退按钮,再点击浏览器前进按钮;

c. 打开新选项卡,输入demo地址,点击#yes的链接,再点击浏览器的后退按钮,再点击浏览器前进按钮;

d. 打开新选项卡,输入demo地址,点击location.hash = ‘#no’的链接,再点击浏览器的后退按钮,再点击浏览器前进按钮。

最后会得到的结果如下:

a. 点击pushState的按钮不会有打印信息,点击后退按钮后会有打印信息,再点击前进按钮会有打印信息;

b. 点击pushState&replaceState的按钮不会有打印信息,点击后退按钮后会有打印信息,再点击前进按钮会有打印信息;

c&d. 点击链接,点击后退按钮,点击前进按钮都会有打印信息。

虽然测试的场景不多,但是也够我们去判断前面那两点结论的正确性了。

比较有意思的是,history.pushState会增加历史记录的条目,但是不会触发hashchange和popstate;hashchange也可以增加历史记录的条目,但是它却可以触发popstate。

前面介绍说到pushState和replaceState的第一个参数stateObj,会与第三个参数对应的历史条目绑定在一块,当popstate事件触发的时候,意味着有新的历史记录条目被激活,在popstate的事件对象里面,有一个state属性,会返回这个激活条目关联的stateObj对象的拷贝。一个历史记录条目只有当它是被pushState创建的,或者用replaceState改过的,才可能有关联的stateObj对象,所以当某些非这2种条件的历史记录条目被激活的时候,可能拿到的stateObj就是null,正如你在demo6里面看到的打印信息显示的那样。

stateObj是会被持久化的硬盘上进行存储的,至少firefox是这么说的,我猜只要历史记录不销毁,它关联的stateObj就会一直存在。所以假如某一个网页在用户最后一次操作后,有关联某个stateObj,那么当用户再次打开这个网页的时候,它的stateObj也是可以被访问的。如果要直接访问当前网页对应条目的stateObj,可以通过history.state属性来访问。

firfox,chrome在页面首次打开时都不会触发popstate事件,但是safari会。。。

popstate事件作用范围仅在于一个document里面,由于pushState和hashchange都不会改变网页的内容也就是document,所以这样的网页里面才能有效使用popstate。假如我们输入一个网页,并且在它里面添加了popstate回调;然后通过链接跳转的方式转到另外一个网页;再点击后退按钮回到第一个网页。这样的情况,第一个网页里面的popstate回调,除了有可能因为页面初始化被触发外,浏览器的后退前进是不会触发它的,因为这种方式改变了窗口的document。

以上就是pushState的相关内容。现在主流的SPA路由主要是靠pushState,它比hashchange的优势,我认为最大的一点就是url的友好性,因为它比hashchange看起来更像是常规的跳转操作,可是体验上又跟hashchange一样,不会给用户造成浏览器发生了刷新的感觉;而且从url的规划层面来说,pushState的url跟原来的url形式都是根据具体场景而定的,hashchange可能就得用同一个url加不同的hash的形式了,这种形式对于系统设计跟seo来说也是不合理的。缺点就是pushState的兼容性没有hashchange那么靠前。要是在移动端,这个自然就不成问题了。

pushState参考资料:

https://developer.mozilla.org/zh-CN/docs/DOM/Manipulating_the_browser_history

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

时间: 2024-08-01 10:33:00

理解浏览器历史记录(2)- hashchange、pushState的相关文章

图解用HTML5的popstate如何玩转浏览器历史记录

一.popstate用来做什么的?简而言之就是HTML5新增的用来控制浏览器历史记录的api. 二.过去如何操纵浏览器历史记录? window.history对象,该对象上包含有length和state的两个值,在它的__proto__上继承有back.forward.go等几个功能函数 在popstate之前,我们可以利用back.forward.go对history进行后退和前进操作. 例如: history.back(); (后退一步,使用history.go(-1)也可实现后退效果) 弊

js之添加浏览器历史记录

如何生成一条历史记录 简单粗暴的方法,直接在当前页面的地址栏中输入地址 点击页面中有a标签的href 执行location.href = 'xxx'(location.replace('xxx')生成一条记录取代当前指针所指向的记录) 表单提交跳转(注意只能跳到当前窗口) 使用pushState方法可以不刷新页面就可以生成一条历史记录,页面URL发生改变 简而言之,只要当页面的URL改变时,就会生成一条历史记录.在IE8及更高的版本中.Opera.Firefox.Chrome.Safari3及更

浏览器历史记录的返回

history.go(+1);禁止返回 location.replace(url);------------------------>清除历史记录. 浏览器历史记录的返回

彻底理解浏览器的缓存机制(http缓存机制)

一.概述 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下HTTP报文,HTTP报文分为两种: 同步sau交流学习社区(首发):https://www.mwcxs.top/page/565.html 1.HTTP请求(Request)报文,报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体),如下图 HTTP响应(Response)报文,报文

理解浏览器的重绘与回流(repaint&amp;&amp;reflow)

今天在做练习的时候,遇到了重绘与回流这个词,表示连个毛都没有听过.遂查之,首先将网上的(http://blog.sina.com.cn/s/blog_8dace7290102wezv.html)关于这两个词的领悟粘贴如下: 一.  浏览器渲染过程 渲染:就是把浏览器把HTML代码以css定义的规则显示在浏览器窗口的过程 浏览器解析HTML的基本过程: 1  用户输入网址,浏览器向服务器发出请求,服务器返回html文件 2  浏览器载入html代码,发现标签内有一个标签引用外部css文件 3 浏览

深入理解浏览器会话机制(session &amp;&amp; cookie)

对于一个前端开发者,cookie我想大家都不陌生,经常会封装一些诸如setcookie,getcookie的方法,session就好像一个最熟悉的陌生人一样,在我们与后端开发者合作项目的时候会用到它,但是不理解它的本质,下面我们就来详细探讨下 cookie 存储位置:存储于客户端 作用:本域跨页面存储数据(我们似乎一般都用来username,passward) cookie一般包含如下图的信息: 传输:下面是一条http请求报文 在每次发送请求中,cookie都会随着http报文发向后台 coo

深入理解浏览器兼容性模式

摘要:关于各种浏览器模式,网上已经有许多文档和资料了,但是很少有能够完全将几个概念阐述清楚的.大部分的资料稍显过时,有些内容可能已经不再适用了.本文中笔者将尽可能将几个概念阐述清楚,并去掉一些过时的内容,仅保留必要的干货. 想必你一定知道浏览器有个标准(Standards)模式和一个怪异(Quirks)模式,或许你还听说过有个"准标准(Almost Standards)"模式.而当你打开Internet Explorer的时候,又看到了什么浏览器模式.文档模式,还有什么兼容性视图等等.

从文档流角度理解浏览器页面渲染引擎对元素定位的解析

文档流:将窗体自上而下分成一行一行,并在每行中按从左至右的挨次排放元素,即为文档流. 我们在排列元素时,遵循"流式结构",即元素遵循从上向下,从左向右堆叠的规则,所以我们在排列元素时如果每行从左往右的元素的总宽度大于窗口的宽度时,就会默认换行. 有三种状况将使得元素离开文档流而存在,分别是浮动.绝对定位.固定定位. 浮动时,离开文档流后的元素,不占用文档流的空间,不会被文档流中的元素发现,离开文档流元素后面的还在文档流上元素会自动上来填补位置接上文档流.此时,离开文档流的元素如同浮在文

理解浏览器的三种模式以及相应的触发条件

严格模式与混杂模式——如何触发这两种模式,区分它们有何意义.在标准模式中,浏览器根据规范呈现页面:在混杂模式中,页面以一种比较宽松的向后兼容的方式显示.浏览器根据DOCTYPE是否存在以及使用的哪种DTD来选择要使用的呈现方法.如果XHTML文档包含形式完整的DOCTYPE,那么它一般以标准模式呈现.对于HTML 4.01文档,包含严格DTD的DOCTYPE常常导致页面以标准模式呈现.包含过渡DTD和URI的DOCTYPE也导致页面以标准模式呈现,但是有过渡DTD而没有URI会导致页面以混杂模式