理解浏览器历史记录(2)-hashchange、pushState
本文也是一篇基础文章。继上文之后,本打算去研究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的更多相关文章
- 图解用HTML5的popstate如何玩转浏览器历史记录
一.popstate用来做什么的?简而言之就是HTML5新增的用来控制浏览器历史记录的api. 二.过去如何操纵浏览器历史记录? window.history对象,该对象上包含有length和stat ...
- js之添加浏览器历史记录
如何生成一条历史记录 简单粗暴的方法,直接在当前页面的地址栏中输入地址 点击页面中有a标签的href 执行location.href = ‘xxx’(location.replace(‘xxx’)生成 ...
- 彻底理解浏览器的缓存机制(http缓存机制)
一.概述 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下HTTP报文,HTTP报文分为两种: 同步s ...
- 每次用 selenium 操作浏览器都还原了 (比如没有浏览器历史记录)
每次用 selenium 操作浏览器都还原了 (比如没有浏览器历史记录)
- Nodejs第一天-{Nodejs基础 深刻理解浏览器 环境变量 基础语法}
Nodejs第一天 1.什么是Nodejs Nodejs是一个可以运行(解析)ECMAScript的环境; ECMAScript是规定了一些列的语法 ,这些语法想要解析的执行就需要放在某个环境 ...
- (转自360安全客)深入理解浏览器解析机制和XSS向量编码
(译者注:由于某些词汇翻译成中文后很生硬,因此把相应的英文标注在其后以便理解.这篇文章讲的内容很基础,同时也很重要,希望对大家有所帮助.) 这篇文章将要深入理解HTML.URL和JavaScript的 ...
- 基于CefSharp开发浏览器(九)浏览器历史记录弹窗面板
一.前言 前两篇文章写的是关于浏览器收藏夹的内容,因为收藏夹的内容不会太多,故采用json格式的文本文件作为收藏夹的存储方式. 关于浏览器历史记录,我个人每天大概会打开百来次网页甚至更多,时间越长历史 ...
- 使用 JavaScript 操作浏览器历史记录 API
History 是 window 对象中的一个 JavaScript 对象,它包含了关于浏览器会话历史的详细信息.你所访问过的 URL 列表将被像堆栈一样存储起来.浏览器上的返回和前进按钮使用的就是 ...
- 理解浏览器的重绘与回流(repaint&&reflow)
今天在做练习的时候,遇到了重绘与回流这个词,表示连个毛都没有听过.遂查之,首先将网上的(http://blog.sina.com.cn/s/blog_8dace7290102wezv.html)关于这 ...
随机推荐
- HTML DOM 对象
本篇主要介绍HTML DOM 对象:Document.Element.Attr.Event等4个对象. 目录 1. Document 对象:表示文档树的根节点,大部分属性和方法都是对元素进行操作. 2 ...
- 了解PHP中的Array数组和foreach
1. 了解数组 PHP 中的数组实际上是一个有序映射.映射是一种把 values 关联到 keys 的类型.详细的解释可参见:PHP.net中的Array数组 . 2.例子:一般的数组 这里,我 ...
- 利用Oracle RUEI+EM12c进行应用的“端到端”性能诊断
概述 我们知道,影响一个B/S应用性能的因素,粗略地说,有以下几个大的环节: 1. 客户端环节 2. 网络环节(可能包括WAN和LAN) 3. 应用及中间层环节 4. 数据库层环节 能够对各个环节的问 ...
- SQL Server-聚焦计算列或计算列持久化查询性能(二十二)
前言 上一节我们详细讲解了计算列以及计算列持久化的问题,本节我们依然如前面讲解来看看二者查询性能问题,简短的内容,深入的理解,Always to review the basics. 持久化计算列比非 ...
- js:给定两个数组,如何判断他们的相对应下标的元素类型是一样的
题目: 给Array对象原型上添加一个sameStructureAs方法,该方法接收一个任意类型的参数,要求返回当前数组与传入参数数组(假定是)相对应下标的元素类型是否一致. 假设已经写好了Array ...
- SignalR SelfHost实时消息,集成到web中,实现服务器消息推送
先前用过两次SignalR,但是中途有段时间没弄了,今天重新弄,发现已经忘得差不多了,做个笔记! 首先创建一个控制台项目Nuget添加引用联机搜索:Microsoft.AspNet.SignalR.S ...
- 【转】外部应用和drools-wb6.1集成解决方案
一.手把手教你集成外部应用和drools workbench6.1 1. 首先按照官方文档安装workbench ,我用的是最完整版的jbpm6-console的平台系统,里面既包含j ...
- java web学习总结(五) -------------------servlet开发(一)
一.Servlet简介 Servlet是sun公司提供的一门用于开发动态web资源的技术. Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向 ...
- android_m2repository_rxx.zip下载地址以及MD5
地址 MD5 https://dl-ssl.google.com/android/repository/android_m2repository_r08.zip 8C8EC4C731B7F55E646 ...
- 在centos7(EL7.3 即 kernel-3.10.0-514.X )上安装BCM4312无线网卡驱动要注意的问题
我新装的centos7主机无法使用里面自带的网卡,查询后发现网卡型号为BCM4312.我在看资料安装的过程中遇到了些问题,纠结了好久,现在分享下要注意的点,为后来的遇到同样问题的人提供点帮助.现在开始 ...