HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL。这个功能很有用,例如通过一段JavaScript代码局部加载页面的内容,你希望通过改变当前页面的URL来反应出页面内容的变化,这时该功能可以派上用场。
举个例子,当用户从首页进入帮助页面时,我们通过Ajax来加载帮助页面的内容。然后这个用户又转到产品页面,我们需要再一次通过Ajax请求来替换页面的内容。当用户想分享页面的URL时,通过History API,我们可以改变页面的URL来反应内容的修改,这样不管是用户分享还是保存的URL都能和页面的内容对应起来。
基本知识
要查看这个API提供了哪些功能非常简单,打开浏览器的Developer Tools工具面板,然后在console中输入history。如果你的浏览器支持History API,你将会看到这个对象下面附带了很多方法。
注意其中的pushState和replaceState这两个方法。我们可以在console中进行一些简单的测试,来看看当我们使用这两个方法时URL会发生什么变化。稍后我们将分析这两个方法中的所有参数,现在我们只需要关注最后一个参数:
history.replaceState(null, null, 'hello');
上面代码中的replaceState方法改变了当前页面的URL,在后面添加了一个'/hello'。不过并没有发出任何request请求,当前窗口仍然停留在之前的页面。不过这里有个问题,当你点击浏览器的后退按钮时,页面并不会回退到我们通过replaceState方法修改之前的那个URL,而是直接回退到了上一个页面(即我们进入到这个页面之前的那个页面)。这是因为replaceState方法不会修改浏览器的history,它只是简单地替换了地址栏中的URL。
要解决这个问题我们需要使用pushState方法:
history.pushState(null, null, 'hello');
现在再点击浏览器的后退按钮,你会发现它和你预想的效果一样。因为pushState方法将我们传给它的URL添加到浏览器的history中,从而改变了浏览器的history。假如我们将另外一个完整的站点URL传递给它会发生什么情况呢?例如我们在baidu.com的首页进行测试,然后在console中输入下面的内容。
history.pushState(null, null, 'https://twitter.com/hello');
浏览器会报错。因为传递给pushState方法的URl必须和当前页面的URL属于同一个源(即不能跨域),否则会有很大的安全漏洞,开发人员可能会借用该功能来欺骗用户,让他们觉得自己是在访问一个完全不同的站点,而事实并非如此。
来看看传递给pushState方法的所有参数:
history.pushState([data], [title], [url]);
- 第一个参数用来传递我们需要的数据,当页面的状态发生变化时我们可以接收到该数据。如用户点击浏览器的后退和向前按钮。需要注意的是在Firefox中只允许传递最多640K的数据。
- 第二个参数title是一个字符串,不过截止到目前,几乎所有的浏览器都忽略该参数。
- 最后一个参数是我们想要替换的URL。
简单回顾一下
这些History API最主要的功能就是不重新加载页面。以往我们只能通过改变window.location的值来修改当前页面的URL,不过这会导致整个页面被重新加载。如果你修改的只是URL中的hash,则不会导致页面被刷新。
使用旧的hashbang方法可以改变页面的URL而不刷新页面。著名的Twitter就是使用的该方法,不过也广受诟病,毕竟hash在location中并不被作为一个真正的资源来对待。
作为History API的早期支持者,Twitter后来抛弃了传统的hashbang方法。在2012年,Twitter的团队介绍了他们的新方法,并列出了其中的一些问题同时还详细地介绍了各浏览器应该如何实现该规范。
一个使用pushState和Ajax的例子
https://css-tricks.com/examples/State/
在该示例中,我们希望用户通过我们的网站找到电影捉鬼敢死队(一部美国电影)中的演员。当用户选择一个图片时,我们需要在下方显示该演员对应的文字描述,同时给该图片一个被选中的效果。当点击后退按钮时,页面应该切换到上一个被选中的图片状态,同时图片下方的文字也要一并切换。当点击前进按钮时也一样。
这里有一个效果图:
这个示例的HTML代码非常简单:div.gallery中包含了所有的链接,每个链接里有一个图片。接下来我们放置了一个空的div.content,用来存放当演员图片被点击时显示在下放的文字。
<div class="gallery"> <a href="https://cdn.css-tricks.com/peter.html"> <img src="bill.png" alt="Peter" class="peter" data-name="peter"> </a> <a href="https://cdn.css-tricks.com/ray.html"> <img src="ray.png" alt="Ray" class="ray" data-name="ray"> </a> <a href="https://cdn.css-tricks.com/egon.html"> <img src="egon.png" alt="Egon" class="egon" data-name="egon"> </a> <a href="https://cdn.css-tricks.com/winston.html"> <img src="winston.png" alt="Winston" class="winston" data-name="winston"> </a> </div> <p class="selected">Ghostbusters</p> <p class="highlight"></p> <div class="content"></div>
如果没有JavaScript该页面仍然可以正常工作,点击图片可以跳转到对应的页面,然后点击后退按钮也可以回到之前的页面。这是为了考虑页面的可访问行和优雅降级。
接下来我们要添加JavaScript代码了。我们通过event propagation给div.gallery元素中的每一个link添加一个事件处理程序,像这样:
var container = document.querySelector('.gallery'); container.addEventListener('click', function(e) { if (e.target != e.currentTarget) { e.preventDefault(); // e.target is the image inside the link we just clicked. } e.stopPropagation(); }, false);
在if语句中,我们获取到被选中图片的data-name属性的值,然后将'.html'添加到后面拼成一个要访问的页面地址,并将其作为第三个参数传递给pushState方法(不过在真实的例子中我们可能会在Ajax请求成功之后才会去修改URL)。
var data = e.target.getAttribute('data-name'), url = data + ".html"; history.pushState(null, null, url); // 此处更改当前的classes样式 // 然后使用data变量的值更新 // 并通过Ajax请求.content元素的内容 // 最后再更新当前文档的title
(当然,此处我们也可以直接使用link的href属性的值)
我将真实代码中的内容都替换成注释了,这样我们可以只关注pushState方法的使用。
现在我们点击图片,URL和Ajax请求的内容会被自动更新,但是当我们点击后退按钮时并不会回退到之前选中的演员图片。这里我们还需要在用户点击后退和前进按钮时使用另外一个Ajax请求来更新内容,并再一次使用pushState方法来更新页面的URL。
我们使用pushState方法中的第一个参数(其中的state)来保存状态信息:
history.pushState(data, null, url);
上面代码中的data参数在popstate事件触发时可以被获取到。当浏览器的后退和前进按钮被点击时会触发popstate事件。
window.addEventListener('popstate', function(e) { // e.state表示上一个被点击的图片的data-attribute });
我们可以通过该参数传递一些我们需要的信息,例如在该示例中我们将之前选中的捉鬼敢死队的演员作为参数传递给requestContent方法,在该方法中,我们使用jQuery的load方法进行一次Ajax请求。
function requestContent(file) { $('.content').load(file + ' .content'); } window.addEventListener('popstate', function(e) { var character = e.state; if (character == null) { removeCurrentClass(); textWrapper.innerHTML = " "; content.innerHTML = " "; document.title = defaultTitle; } else { updateText(character); requestContent(character + ".html"); addCurrentClass(character); document.title = "Ghostbuster | " + character; } });
如果用户点击了演员Ray的图片,event listener会被触发,然后在pushState事件中保存图片的data属性的值。当用户点击另外一个图片,并点击了浏览器的后退按钮,此时popstate事件会被触发,从而重新加载ray.html页面。
这意味着什么呢?当我们点击一个演员的图片然后将被更改的URL分享出去,用户访问这个URL时对应的HTML文件会被自动加载进来。这会带来一些更好的用户体验,并保证了URL和页面内容的一致性从而减少了因此而带给用户的一些困惑。
上面的示例只是简单地通过jQuery来动态加载内容,我们当然也可以在pushState方法中传递一些更加复杂的对象。不过这个例子已经能足够说明问题并帮助我们开始学习如何使用History API的功能。我们先要学会走,然后才能跑。
下一步
如果我们想大范围地使用这种技术,我们应该考虑使用一些专有的工具,例如pjax。 它是一个jQuery的插件,使用它可以大大提高我们同时使用Ajax和pushState方法进行开发的速度,不过它只支持那些使用History API接口的现代浏览器。
History JS可以兼容旧浏览器,对于不支持History API接口的浏览器,它依然使用旧的URL hash的方式来实现同样的功能。
有关URLs
这里我特别引用了Kyle Neath有关URLs的说明:
URLs是一个通用的概念,它可以工作在Firefox, Chrome, Safari, Internet Explorer, curl, wget,甚至在你的iPhone, Android以及便签纸上。它是web中的一个通用的语法。不要认为这是理所当然的。任何一个稍微懂点技术的用户都可以浏览你的应用的90%以上的部分而不用去刻意记住那些URL的结构。要实现这样的效果,你需要考虑URLs的实用性。
这意味着不论你想要进行什么样的hacks或性能优化,作为web开发人员,你应该注重URL。而随着HTML5 History API的帮助,我们可以轻松地解决诸如上述示例中的一些问题。
常见问题
- 将Ajax请求的地址嵌入到a标记的href属性中通常是个不错的主意。
- 确保在JavaScript的click事件处理程序中return true,这样当有人使用中键点击或命令点击时不会导致程序被意外覆盖。
补充
- Mozilla有关操作浏览器history的文档
- Ajax示例集锦Dive into HTML5
- Twitter有关pushState的实现
浏览器支持
Chrome | Safari | Firefox | Opera | IE | Android | iOS |
---|---|---|---|---|---|---|
31+ | 7.1+ | 34+ | 11.50+ | 10+ | 4.3+ | 7.1+ |