阿瑞斯的BLOG

HTML5 historyAPI

源起
学习webpack使用的时候,写简单SPA应用时需要通过pushState API变更历史URL,并加载页面

是什么?

访问历史记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* description: 向历史记录里添加一条历史记录,此时地址栏发生变化,历史记录里添加了一条状态,但是页面并不会刷新也不会跳转,!!!注意此时新增的状态为当前通过pushState添加的,而历史记录里的是之前的状态被压栈
* summary: 存储当前状态
* @param: {Object} state state为一个对象或者null,称为状态对象。这个对象可以拿来记录当前URL所对应的一些信息,状态对象由pushState创建,在popstate事件触发的时候通过e.state拿到,总而言之,它存储JSON字符串,可以用在popstate事件中。
* @param {String} title title为页面的标题,但当前所有浏览器都忽略这个参数,传递一个空字符串是安全的做法。当然,你可以传递一个短标题给你要转变成的状态。
* @url {String} url url则为新历史记录的地址,不写则为当前页,请注意,浏览器在调用pushState()方法后不会去加载这个URL,并且这个地址和当前页必须是同源的
*/
history.pushState(state, title, url)
// 参数同上
history.replaceState(state, title, url)
相关事件:popstate
何时触发?当历史记录条目发生更改时,注意这个更改指的是点击了浏览器历史记录的前进/后退或者是调用history.back()/history.go()/history.forward()时。需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。而popstate事件的state属性就是历史状态对象的副本。

有什么用?

界面上的所有JS操作不会被浏览器记住,就无法回到之前的状态,在HTML5可以通过window.history操作访问历史状态,让一个页面可以有多个历史。

为什么会有?

假设一个学生管理系统的记录的页面,然后我们需要把这个页面学生违纪的内容发送给他人,但是如果仅仅只是一个tab栏切换,我们发送URL地址就会有一个尴尬的现象,就是发过去的又是初始化的页面(也就可以理解为:无状态),因为URL地址至始至终没有发生变化过。

简单来说就是动态加载出来的内容我们无法给别人分享链接。这种对状态的需求在应用开发中尤为迫切!!!

因为AJAX的兴起,前端走向了web应用的时代,利用AJAX动态加载实现内容替换。但是弊端是利用AJAX实现无刷新改变的文档内容,是不会修改URL的,但是这里有人要问为什么一定要修改URL呢?因为一个URL代表一个特定的网络资源,AJAX修改了页面的内容,所以用不同的URL去标识他们,这个是非常有必要的。

这种状态思想特别是在现在web开发(web应用)中是非常重要的。

以前的history有什么?

之前我们可以通过history对象实现前进,后退,和刷新之类的操作

1
2
3
4
history.length; // 历史堆栈中的记录数
history.back(); // 后退
history.forward(); // 前进
history.go([delta]):delta是个数字,如果不写或为0,则刷新本页;如果为正数,则前进到相应数目的页面;若为负数,则后退到相应数目的页面。

demo1—基本用法和效果

1
2
3
4
5
6
7
8
9
10
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
history.pushState({page: 1}, "title 1", "?page=1"); // 添加一条状态,此时地址栏变为url+"?page=1",之前的入口页被压入历史记录
history.pushState({page: 2}, "title 2", "?page=2"); // 添加第二条状态,此时上一个状态被添加到历史记录,此时历史记录里应该有两个状态
// history.replaceState({page: 3}, "title 3", "?page=3");
history.replaceState({page: 3}, "title 3", "http://www.baidu.com"); // [1] 替换当前状态信息,此时因为没有添加,所以历史记录里还是两条状态信息
// history.back(); // [2] alerts "location: http://example.com/example.html?page=1, state: {"page":1}" 通过结果可以看出replaceState只是替换了当前状态而不是替换当前,追加之前到历史栏
// history.back(); // [3] alerts "location: http://example.com/example.html, state: null 通过结果可以看出当第二次回退,实际上回到了最初的入口页,这个状态在页面载入,脚本执行后就被放入了历史记录里
// history.go(2); // [4] alerts "location: http://example.com/example.html?page=3, state: {"page":3} 通过结果可以看出page2确实被page3替换掉了

对pushState的误解

一开始误认为pushState会直接向地址栏加入我们通过调用pushState()添加的状态,而pushState()其实是改变了当前状态,把当前状态前一个状态添加到历史记录。

源自张鑫旭老师的historyAPI demo

配合ajax技术实现的PJAX技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
JS代码:
// 获取元素,绑定事件,获取查询参数
var eleMenus = $("#choMenu a").bind("click", function(event) {
var query = this.href.split("?")[1];
if (history.pushState && query && !$(this).hasClass(clMenuOn)) {
/*
ajax载入~~
*/
// history处理
var title = "上海3月开盘项目汇总-" + $(this).text().replace(/\d+$/, "");
document.title = title;
if (event && /\d/.test(event.button)) {
history.pushState({ title: title }, title, location.href.split("?")[0] + "?" + query);
}
}
return false;
});
//
var fnHashTrigger = function(target) {
var query = location.href.split("?")[1], eleTarget = target || null;
if (typeof query == "undefined") {
if (eleTarget = eleMenus.get(0)) {
// 如果没有查询字符,则使用第一个导航元素的查询字符内容
history.replaceState(null, document.title, location.href.split("#")[0] + "?" + eleTarget.href.split("?")[1]) + location.hash;
fnHashTrigger(eleTarget);
}
} else {
eleMenus.each(function() {
if (eleTarget === null && this.href.split("?")[1] === query) {
eleTarget = this;
}
});
if (!eleTarget) {
// 如果查询序列没有对应的导航菜单,去除查询然后执行回调
history.replaceState(null, document.title, location.href.split("?")[0]);
fnHashTrigger();
} else {
$(eleTarget).trigger("click");
}
}
};
if (history.pushState) {
window.addEventListener("popstate", function() {
fnHashTrigger();
});
// 默认载入
fnHashTrigger();
}

使用pushState实现有状态的tab栏切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>History Api</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
aside {
background-color: #ccc;
width: 220px;
float: left;
height: 100%;
}
aside ul {
font-size: 20px;
line-height: 2;
}
aside ul li {
cursor: pointer;
}
article {
background-color: #f5f5f5;
margin-left: 220px;
padding: 20px;
height: 100%;
overflow: scroll;
font-size: 20px;
}
</style>
</head>
<body>
<aside>
<ul id="list" data-id="1" data-name="sss">
</ul>
</aside>
<article>
<p id="content"></p>
</article>
<!-- plugin:auto file name -->
<script src="data.js"></script>
<script>
(function() {
var listElement = document.querySelector('#list');
// 遍历数据并渲染到页面
for (var title in data) {
var liElement = document.createElement('li');
liElement.innerHTML = '⭐️' + title;
liElement.setAttribute('data-title', title);
listElement.appendChild(liElement);
}
var liElements = document.querySelectorAll('#list>li');
var content = document.querySelector('#content');
// 注册每一个元素事件,点击时保存当前状态
for (var i = 0; i < liElements.length; i++) {
liElements[i].addEventListener('click', function() {
// 拿到被点击title
var title = this.dataset['title'];
// 赋值
content.innerHTML = data[title];
// 操作历史记录
if (window.history && history.pushState) {
// 添加一个新的历史记录
history.pushState(title, 'title没有任何浏览器支持', '?t=' + title);
} else {
console.log('不支持');
}
});
// 当我们在伪造的访问历史中前进或后退时会执行一个popstate事件
window.addEventListener('popstate', function(e) {
// 回退时根据保存的状态渲染页面内容
content.innerHTML = data[e.state];
});
// window.location = "https://www.baidu.com";
// 第一次请求过来 获取地址栏中的t参数
// window.location可以拿到当前网页中跟地址相关的信息
var search = window.location.search; // ?t=jkaljdksfla
// 如果地址栏中的地址有中文,会以URL编码方式呈现
// decodeURI 可以转换到之前中文
var title = search.split('=')[1]; // ['?t','jkaljdksfla']
if (title) {
// 有值 decodeURI作用就是从URL编码转换到之前的状态
console.log(decodeURI(title));
content.innerHTML = data[decodeURI(title)];
}
})();
</script>

此例子最大的效果在于:保存了状态,当把URL发送给别人的时候,得到的是一个带有状态的视图,而不是初始化页面。

参考:

  1. popstate·MDN

  2. ajax与HTML5 history pushState/replaceState实例·张鑫旭

  3. PJAX的实现与应用·小胡哥