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

1. hashchange

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

1)直接更改浏览器地址,在最后面增加或改变#hash;
2)通过改变location.href或location.hash的值;
3)通过触发点击带锚点的链接;
4)浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。

假如我们还用上文的demo来测试,并按照以下步骤操作的话:
打开新选项卡;输入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

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

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

    一.popstate用来做什么的?简而言之就是HTML5新增的用来控制浏览器历史记录的api. 二.过去如何操纵浏览器历史记录? window.history对象,该对象上包含有length和stat ...

  2. js之添加浏览器历史记录

    如何生成一条历史记录 简单粗暴的方法,直接在当前页面的地址栏中输入地址 点击页面中有a标签的href 执行location.href = ‘xxx’(location.replace(‘xxx’)生成 ...

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

    一.概述 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下HTTP报文,HTTP报文分为两种: 同步s ...

  4. 每次用 selenium 操作浏览器都还原了 (比如没有浏览器历史记录)

    每次用 selenium 操作浏览器都还原了 (比如没有浏览器历史记录)

  5. Nodejs第一天-{Nodejs基础 深刻理解浏览器 环境变量 基础语法}

    Nodejs第一天 1.什么是Nodejs ​ Nodejs是一个可以运行(解析)ECMAScript的环境; ​ ECMAScript是规定了一些列的语法 ,这些语法想要解析的执行就需要放在某个环境 ...

  6. (转自360安全客)深入理解浏览器解析机制和XSS向量编码

    (译者注:由于某些词汇翻译成中文后很生硬,因此把相应的英文标注在其后以便理解.这篇文章讲的内容很基础,同时也很重要,希望对大家有所帮助.) 这篇文章将要深入理解HTML.URL和JavaScript的 ...

  7. 基于CefSharp开发浏览器(九)浏览器历史记录弹窗面板

    一.前言 前两篇文章写的是关于浏览器收藏夹的内容,因为收藏夹的内容不会太多,故采用json格式的文本文件作为收藏夹的存储方式. 关于浏览器历史记录,我个人每天大概会打开百来次网页甚至更多,时间越长历史 ...

  8. 使用 JavaScript 操作浏览器历史记录 API

    History 是 window 对象中的一个 JavaScript 对象,它包含了关于浏览器会话历史的详细信息.你所访问过的 URL 列表将被像堆栈一样存储起来.浏览器上的返回和前进按钮使用的就是 ...

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

    今天在做练习的时候,遇到了重绘与回流这个词,表示连个毛都没有听过.遂查之,首先将网上的(http://blog.sina.com.cn/s/blog_8dace7290102wezv.html)关于这 ...

随机推荐

  1. 视频 - 在 VirtualBox 中部署 OpenStack

    大家新年好,CloudMan 今天给大家带来一件新年礼物. 一直以来大家都反馈 OpenStack 学习有两大障碍:1. 实验环境难搭2. 体系复杂,难道大今天我就先帮大家解决环境问题.前两天我抽空在 ...

  2. 【Web动画】SVG 线条动画入门

    通常我们说的 Web 动画,包含了三大类. CSS3 动画 javascript 动画(canvas) html 动画(SVG) 个人认为 3 种动画各有优劣,实际应用中根据掌握情况作出取舍,本文讨论 ...

  3. .NET跨平台之运行与Linux上的Jexus服务器

    谈及.NET跨平台,已经不是什么稀奇的事儿.今天我们就以Jexus服务器的部署为例.简单示范下.在这里,我用VMWare虚拟机来搭建Linux运行环境. Linux,我们选择CentOS7.大家可以前 ...

  4. 【NLP】Python NLTK处理原始文本

    Python NLTK 处理原始文本 作者:白宁超 2016年11月8日22:45:44 摘要:NLTK是由宾夕法尼亚大学计算机和信息科学使用python语言实现的一种自然语言工具包,其收集的大量公开 ...

  5. Oracle安装部署,版本升级,应用补丁快速参考

    一.Oracle安装部署 1.1 单机环境 1.2 Oracle RAC环境 1.3 Oracle DataGuard环境 1.4 主机双机 1.5 客户端部署 二.Oracle版本升级 2.1 单机 ...

  6. 反应器(Reactor)和主动器(Proactor)

    网络方面用的比较多的库是libevent和boost.asio,两者都是跨平台的.其中libevent是基于Reactor实现的,而boost.asio是基于Proactor实现的.Reactor和P ...

  7. 微信开发笔记(accesstoken)

    access_token分两种 一种是公众号权限获取用,调用cgi-bin接口 ,此种token一个公众号同时只有一个,用这一个就够了. 服务器最好缓存. 用这个token前提是用户关注了此公众号. ...

  8. 浅析SQL查询语句未显式指定排序方式,无法保证同样的查询每次排序结果都一致的原因

    本文出处:http://www.cnblogs.com/wy123/p/6189100.html 标题有点拗口,来源于一个开发人员遇到的实际问题 先抛出问题:一个查询没有明确指定排序方式,那么,第二次 ...

  9. Linux基础介绍【第二篇】

    远程连接Linux的原理 SHH远程连接介绍 当前,在几乎所有的互联网企业环境中,最常用的Linux提供远程连接服务的工具就是SSH软件,SSH分为SSH客户端和SSH服务端两部分.其中,SSH服务端 ...

  10. centos7 安装时候检测不到空余硬盘的解决办法

    我是用U盘装的centos,在进行硬盘规划时,看到硬盘的可用空间太少 这是因为我的硬盘以前装的是windows系统,硬盘几乎都已经被windows 操作系统给使用了,剩余空间也只会是windows用剩 ...