本文详解如何利用 HTML5 History API 在不刷新页面的情况下实现浏览器前进后退功能。重点解析 pushState 和 replaceState 方法,并针对不支持该 API 的老版本浏览器,提供基于轮询和观察者模式的 URL 监听兼容方案,帮助开发者构建流畅的单页面应用体验。
单页面应用与 AJAX 的历史导航挑战
在深入理解 Backbone 框架之前,首先需要掌握单页面应用(SPA)的核心概念。SPA 的关键在于如何在保持页面不刷新的前提下,动态修改浏览器地址栏中的 URL。
相比传统页面跳转,AJAX 技术显著提升了用户体验,避免了页面切换时的白屏闪烁。然而,早期的 AJAX 应用往往缺乏对浏览器前进/后退按钮的支持。这导致用户在网站内浏览后,若刷新页面便会回到初始位置,且无法通过浏览器导航按钮查看历史记录。
解决状态持久化的问题相对简单,只需利用 Cookie 或 localStorage 记录应用状态,页面刷新时读取该状态并重新发起相应的 AJAX 请求即可。但解决浏览器历史导航问题则更为复杂,下文将介绍现代浏览器的解决方案。
HTML5 History API 详解
HTML5 解决方案
要理解 HTML5 如何实现前进后退功能,必须先了解 history 对象和 location 对象。
History 对象
History 对象属性
1. length:返回浏览器历史列表中 URL 的数量。每在当前标签页访问一个新页面,该数值加 1。出于隐私保护考虑,具体的 URL 内容不可见。 2. state:与当前网址关联的状态对象,只能通过 pushState 和 replaceState 方法进行添加或修改。利用此属性可以存储与 URL 相关的信息。
History 对象方法
1. history.back()
该方法不接受参数,调用后返回上一个浏览的页面,效果等同于点击浏览器的后退按钮。
2. history.forward()
该方法不接受参数,调用后返回后退前浏览的页面,效果等同于点击浏览器的前进按钮。
3. history.go(number)
该方法接受一个整数参数。history.go(-1) 相当于后退一页,history.go(1) 相当于前进一页,而 history.go(0) 则会刷新当前页面。
4. history.pushState(state, title, url)
这是在不刷新页面前提下改变 URL 的关键方法。执行此方法会更新当前页面的 location.href 并修改 history.state 对象,同时使 history.length 增加 1。该方法接收三个参数:
1. state:与当前网址相关的状态对象。 2. title:页面标题,但所有主流浏览器目前均忽略此参数,如需改变标题仍需使用 document.title。 3. url:一个与当前页面同域的 URL,执行后 location.href 将变为该值。
5. history.replaceState(state, title, url)
此方法参数与 pushState 相同,但它不会改变 history.length,仅修改当前的 history.state 和 location.href。
需要注意的是,pushState 和 replaceState 的第三个参数(url)不可跨域,且在测试中(如 Chrome 33)不会触发浏览器的 popstate 事件和 onhashchange 事件。
location 对象与事件监听
location 对象
除了点击前进/后退按钮和监听 history 事件外,还可以通过调用 location 对象的方法或修改其属性来改变 URL:
location 对象的属性(可读写):
1. host:域名加端口号 2. hostname:域名 3. port:端口号 4. protocol:协议 5. href:完整路径 6. origin:协议加域名加端口 7. hash:以井号 (#) 开头的 URL 部分(hash) 8. pathname:文档路径加文档名 9. search:问号 (?) 后面的内容
通过修改 location.href 或 location.hash 可以达到无刷新改变 URL 的目的。
location 对象的方法:
1. assign:改变 URL 的值,并将当前 URL 添加到历史记录中,使 history.length 增加 1。例如 location.assign('#' + x) 会改变 URL 但不刷新页面。 2. reload:刷新页面。 3. replace:改变 URL 的值,但保持 history.length 不变。使用方法与 assign 类似。
popstate 事件
当 URL 发生改变时,例如用户点击前进/后退按钮、调用 history.go(n)(n 不等于 0)或设置 location.hash = x(x 不等于当前 location.hash),都会触发此事件。可以利用该事件监听 URL 变化,从而实现各种功能。
代码如下:
window.onpopstate = function(){
//do sth
}
onhashchange 事件
改变 hash 值会触发 popstate 事件,但触发 popstate 事件不一定会触发 onhashchange 事件。经测试:
1. 若 hash 改变但 location.pathname 不变,会触发 onhashchange 事件,例如 history.pushState('', '', '#abc'); 2. 若 hash 和 location.pathname 同时改变,则不触发,例如 history.pushState('', '', 'a#abc');
老式浏览器的兼容方案
老旧浏览器的写法
老式浏览器不支持 pushState 和 replaceState,因此通过 popstate(实际上也不支持该方法)监听 URL 变化的路径行不通。只能通过改变 URL 中 # 后面的内容来实现无刷新,但由于老浏览器不支持 onhashchange 事件,它们对 URL 变化几乎无反应(除了页面滚动至对应 ID 的位置)。因此,只能采用轮询方案,启动一个 setInterval 来监听 URL 的值。示例如下:
代码如下:
var prevHash = window.location.hash;
var callback = function(){...}
window.setInterval(function() {
if (window.location.hash != prevHash) {
prevHash = window.location.hash;
callback(prevHash);
}
}, 100);
当然,这种写法非常粗糙。如果不考虑点击带有 ID 的 a 标签来改变 hash 的情况,可以利用设计模式更优雅地实现 URL 监听。例如经典的观察者模式,专门创建一个类来负责改变 hash 功能,所有需要监听 URL 变化的类(观察者)去订阅这个类(被观察者)。
代码如下:
//改变url的类
function UrlChanger() {
var _this = this;
this.observers = [];
//添加观察者
this.addObserver = function(obj) {...}
//删除观察者
this.deleteObserver = function(obj) {...}
//通知观察者
this._notifyObservers = function() {
var length = _this.observers.length;
console.log(length)
for(var i = 0; i < length; i++) {
_this.observers[i].update();
}
}
//改变url
this.changeUrl = function(hash) {
window.location.hash = hash;
_this._notifyObservers();
}
}
//监听类
function oneOfObservers() {
var _this = this;
this.update = function() {...}
}
//实现
var o1 = new UrlChanger();
var o2 = new oneOfObservers();
o1.addObserver(o2);
o1.changeUrl('fun/arg1/arg2/');
//o2 has do sth...