原文:MANIPULATING HISTORY FOR FUN & PROFIT · Dive Into HTML5
说明:因个人水平有限,如有误译,欢迎指正。
浏览器地址栏可能是这世上所有用户界面中最复杂的一部分了。广告牌上,火车身上,甚至街头涂鸦上都能看到 URL 的身影。借助于导航按钮(无疑是浏览器中最重要的按钮),您拥有了功能强大的前进和后退的操作,以便地在错综交织的 Web 世界中畅游。
HTML5 history API 是通过脚本操纵浏览器历史的一种标准方式。历史导航功能是此 API 的一部分,并已在 HTML 早期版本中实现。HTML5 为此 API 引入了向浏览器历史中新增条目的功能,这样可以显式地更改浏览器地址栏中的 URL(而不会触发页面刷新),当用户点击浏览器后退按钮的时候,这些条目就会从历史堆栈中移除并触发一个事件。这意味着浏览器地址栏中的 URL 仍将作为当前资源的唯一标识符,即使是在那些甚至没有执行过一个完整页面刷新的脚本密集型应用中。
为什么?
为什么您需要手动修改浏览器地址呢?毕竟,一个简单的链接就能够帮您导航到一个新的 URL,而且这种方式已经伴随着 Web 度过了 20 个年头,并且这种方式仍然有效。HTML5 history API 并不是想要颠覆现有的方式。恰恰相反,近几年,Web 开发者发现了一个新的且令人激动的方式来改变现状而不需要借助于新的标准。HTML5 history API 就是针对脚本密集型应用而设计的。
回到最初的原则,URL 究竟是干什么的?它标识了一个唯一的资源。您可以直接去链接它;可以使用书签收藏;搜索引擎可以对其做索引;可以拷贝粘贴并通过邮件发给别人,任何点击它的人都将看到与你看到的一样的资源。这些都是很棒的特性,也是 URL 重要的原因。
所以我们想要唯一性资源能够有一个唯一的 URL。但是同时,浏览器却有一个基本的限制:如果您更改了 URL,即使是通过脚本,它都会向 Web 服务器触发一个请求并对整个页面执行刷新。耗时又耗费资源,特别是在您导航的页面与当前页面几乎相似的情况下。新页面上所有东西都进行了下载,即使新页面的部分区域与当前页面一样。现如今,没有任何方式告诉浏览器在变更 URL 的时候仅下载页面的某些区域资源。
可是 HTML5 history API 可以协助您实现这样的需求。它不会触发整个页面的刷新,您可以使用脚本下载另外一部分页面。这可能有点难以解释,并且您也需要做一些工作。准备好,可看仔细了!
假设您有两个页面,页面 A 和 页面 B。这两个页面 90% 的内容是相似的,只有 10% 的页面内容是不一样的。用户首先导航到页面 A,然后他尝试导航到页面 B。但是这并没有触发整个页面的刷新,您中断了这个导航并手动执行了以下步骤:
- 从页面 B 下载与页面 A 不同的那 10% 部分((可以使用
XMLHttpRequest
)。这需要服务器端做一些修改。您需要编写代码以返回页面 B 与页面 A 那不同的 10% 部分。这可以是终端用户正常情况下看不到的隐藏的 URL 或查询参数。 - 替换掉不同的部分(使用
innerHTML
或其它的 DOM 方法)。您可能也需要修改被替换部分的元素的事件处理器。 - 使用页面 B 的 URL 更新浏览器地址栏,这需要借助 HTML5 history API 中的一个方法,待会就向您介绍。
这个假象结束时(如果您执行正确),浏览器就有了页面 B 的 DOM,就好比您直接导航到页面 B。但是您从未真正导航到页面 B,并且没有发生整个页面的刷新。这就是假象。但是因为这个“编译的”页面与页面 B 看上去一样并且拥有相同的 URL,那么用户就不会发现有什么不同(也不会了解到您事无巨细地提升他们的用户体验)。
怎么做?
HTML5 history API 是 window.history
对象的一系列方法,加上 window
对象的一个事件。您可以使用这些方法来 测试浏览器对 history API 的支持。
dive into dogs 是一个使用 HTML5 history API 的例子。它展示了一个常见的模式:一篇很长的文章中关联了一个内联的相册。在支持的浏览器中,点击相册上的 Next 和 Previous 链接可以同时更新照片及浏览器地址栏中的 URL,而这个过程不会触发整个页面的刷新。在不支持的浏览器中——或者支持的浏览器却被用户禁用了脚本——那么这个链接就如同普通的链接一样,刷新整个页面以获取一个新的页面。
现在让我们深入学习 dive into dogs 这个示例,看一下它是如何工作的。下面是一个照片相关的标记语言:
这里没有什么不寻常的。照片本身是一个 并放置在
中,链接也是普通的
元素,并且整个放置在一个
中。重点是,它们都是常规的链接并且能够正常运行。后面追加的所有代码就是 HTML history API 侦测脚本。如果用户使用的是一个不支持的浏览器,那么我们的 history API 代码都不会执行,当然这也包括那些禁用脚本的用户。
主驱动器函数将获取这两个链接并将其传递给一个函数,addClicker()
,它将设置自定义的点击处理器来完成实际的回调工作。
function setupHistoryClicks() {
addClicker(document.getElementById("photonext"));
addClicker(document.getElementById("photoprev"));
}
这就是 addClicker()
函数。它接收一个 元素并为其添加一个点击处理器。而这正是点击处理器有趣的地方。
function addClicker(link) {
link.addEventListener("click", function(e) {
swapPhoto(link.href);
history.pushState(null, null, link.href);
e.preventDefault();
}, false);
}
swapPhoto()
函数完成了我们三步假象的前两步。前一半工作是获取导航链接的 URL——casey.html
,adagio.html
等——然后为隐藏的页面构建 URL,也就是下一张照片所需要的标记。
function swapPhoto(href) {
var req = new XMLHttpRequest();
req.open("GET",
"http://diveintohtml5.info/examples/history/gallery/" +
href.split("/").pop(),
false);
req.send(null);
这里就是一个由 http://diveintohtml5.info/examples/history/gallery/casey.html 返回的标记样例(您可以在浏览器中直接访问这个 URL 来查看它)。
这看起来是不是很熟悉?确实是,这就是在原始页面中用于展示第一张图片相同的标记语言。
swapPhoto()
函数的第二部分执行的就是:将刚下载完的标记插入到当前的页面。记住,在整个 figure,photo 和 caption 外面包裹了一个 。所以,插入一个新的图片标记只需要一行代码,将
的
innerHTML
属性设置为返回的 XMLHttpRequest
的 responseText
属性。
if (req.status == 200) {
document.getElementById("gallery").innerHTML = req.responseText;
setupHistoryClicks();
return true;
}
return false;
}
(同样注意对 setupHistoryClicks()
的调用,在新插入进来的导航链接上重设自定义的 click
事件是有必要的。设置 innerHTML
的时候会将旧的链接以及它们的事件处理器全部清除掉。)
现在,让我们返回到 addClicker()
函数。在成功替换图片后,还有一步需要完成:在浏览器地址栏中设置 URL。
history.pushState(null, null, link.href);
history.pushState()
函数接受三个参数:
-
state
可以是任何 JSON 数据结构。它将被传递给popstate
事件处理器。
这个等会就会学到。我们不需要在这个示例中跟踪任何的状态,所以我在这里将其设置为了null
。 -
title
可以是任何字符串。 大多浏览器暂时都没有使用这个参数。如果您想要设置页面标题,您应该将其存在state
参数中,并在popstate
回调中手动设置它。 -
url
可以是任意 URL(必须同源)。这也就是您想要在浏览器地址栏中显示的 URL。
调用 history.pushState
将会立即更改浏览器地址栏中的 URL。那么这就结束了吗?当然,不完全是,我们还需要讨论当用户点击最重要的返回按钮会发生什么事。
正常情况下,当用户导航到一个新的页面(会伴随整个页面的刷新),浏览器会将新的 URL 压入到它的历史堆栈中,然后下载并重绘新的页面。当用户点击返回按钮的时候,浏览器会从它的历史堆栈中弹出一个新的页面并重绘之前的页面。但是现在当您短路这个导航并阻止整个页面刷新的时候发生了什么?您已经假装“向前”导航到了一个新的 URL,那么现在您也需要假装“向后”导航到之前的 URL。假装“向后”导航的关键就是 popstate
事件。
window.addEventListener("popstate", function(e) {
swapPhoto(location.pathname);
});
在您使用了 history.pushState()
函数将一个假的 URL 压入浏览器历史堆栈后,当用户点击后退按钮的时候,浏览器将会在 window
对象上触发一个 popstate
事件。这就是您再一次实现假象的机会。因为使某些东西消失并不够,您还需要将其带回来。
在这个示例中,“带回来”和上面的图片切换一样简单,我们需要做的就是在调用 swapPhoto()
函数的时候传入当前的地址。在 popstate
回调被调用的时候,浏览器地址栏中可见的 URL 已经更改为了之前的 URL。同样,全局的 location
属性已经被更新为之前的 URL。
为了更好的理解它,我们将其从头到尾梳理以下:
- 用户加载 http://diveintohtml5.info/examples/history/fer.html 页面 ,看到了故事和一张 Fer 的照片。
- 用户点击 "Next" 链接,一个
元素,它的
href
属性是 http://diveintohtml5.info/examples/history/casey.html。 - 导航到 http://diveintohtml5.info/examples/history/casey.html 页面时并没有发生整个页面的刷新,
元素上的自定义
click
处理器拦截了点击并执行了它自己的代码。 - 我们自定义的
click
处理器调用了swapPhoto()
函数,它创建了一个XMLHttpRequest
对象并同时下载位于 http://diveintohtml5.info/examples/history/gallery/casey.html 的 HTML 片段。 -
swapPhoto()
函数设置了相册外围元素(一个元素)的
innerHTML
属性,因此将 Fer 的照片替换为了 Casey 的照片。 - 最后,我们自定义的
click
处理器调用了history.pushState()
函数来手动将浏览器地址栏中的 URL 变更为 http://diveintohtml5.info/examples/history/casey.html。 - 用户点击了浏览器中的返回按钮。
- 浏览器注意到 URL 是手动压入到历史堆栈中的。因此,导航到之前的 URL 时并没有重绘整个页面,而是简单地将地址栏中的 URL 更新为之前页面的 http://diveintohtml5.info/examples/history/fer.html,并同时触发了
popstate
事件。 - 我们自定义的
popstate
处理器再次调用了swapPhoto()
函数,这时使用的是浏览器地址栏中可见的 URL。 - 再一次使用
XMLHttpRequest
,swapPhoto()
函数下载了位于 http://diveintohtml5.info/examples/history/gallery/fer.html 的 HTML 片段,并设置了外围元素的
innerHTML
属性,因此将 Casey 的照片又替换回了 Fer 的照片。
这就是完整的阐述。所有可见迹象(页面的内容,地址栏中的 URL)都向用户表明他们已经导航到或回退到了一个页面。但是并没有发生整个页面的刷新——这都是一个精心设计的假象。
延伸阅读
- Session history and navigation in the HTML5 draft standard
- Manipulating the browser history on Mozilla Developer Center
- Simple history API demo
- 20 Things I Learned About Browsers and the Web, an advanced demo that uses the history API and other HTML5 techniques
- Using HTML5 today describes Facebook’s use of the history API
- The Tree Slider describes Github’s use of the history API
- History.js, a meta-API for manipulating history in both newer and older browsers