一. 场景再现
如大家熟知,Ajax 可以实现页面的无刷新操作,但会造成两个与普通页面操作(有刷新地改变页面)有着明显差别的问题—— URL 没有修改以及无法使用前进、后退按钮。例如常见的 Ajax 分页,在第一页点击第二页的链接,Ajax 分页完成后浏览器地址栏上显示的 URL 依然是第一页的 URL,使用后退按钮也无法回到第一页。url 的改变代表一个标识,在传统的网页体验中,内容的变更伴随 url 的改变,url 的改变、前进和后退按钮三者之间更加形成一种独特的导航体验,而 Ajax 破坏了这种体验难免会对用户造成不便。
当然,很早之前,机智的前端工程师们已经想好了解决办法,最为常见的,就是使用 hash ——在 URL 结尾添加形如"#xxx" 的标识,然后用 onhashchange 等方式监听 hash 值变化作出相应处理。
很麻烦?是的,因此,HTML5 History 中新增了几个可以优雅地解决这些问题的特性!
二. HTML5 History 的新特性
History 对象从 HTML4 开始引入,HTML5 中增加了 pushState, replaceState 两个方法,和 popstate 事件。下面作一些简单的介绍。
1. pushState()方法
pushState() 的作用是往历史记录的堆栈中压入一条记录,该方法有三个参数:
state object —— 一个对象,用于保存状态信息,当 popstate 事件被触发时,popstate 事件对象的 state 属性会包含相应的 state object 的拷贝。state object 的容量很小(Firefox 中强制为 640k),如果需要储存较大的数据,建议使用 localStorage 或 sessionStorage。
title —— 即被压入的历史记录的页面的标题,该属性暂时被所有浏览器忽略,实际开发时可以填空字符或一个简短的标题。
url —— 新的历史记录的地址,可以是相对路径或绝对路径,若为相对路径则以当前 url 为基址。
2. replaceState()方法
replaceState() 方法与 pushState() 方法类似,参数与 pushState() 也相同,但 replaceState() 方法会修改当前的历史记录而并非创建新的记录,因此在需要更新当前历史记录的 state object 或 URL 时,使用该方法会更加合适。
3. popstate 事件
popstate 事件会在激活的历史记录发生变化(如前进、后退、调用 pushState 或 replaceState 方法)时触发在 window 对象上。如上面所描述,如果被激活的历史记录由 pushState 创建或是被 replaceState 修改,则 popstate 事件的状态属性将包含相应的 state object 的拷贝,开发者可以在 popstate 的回调中调用这些之前保存在 state object 中的信息。
值得注意的是,Chrome 会在打开页面(包括第一次打开页面)以及页面刷新时产生 popstate 事件而 Firefox 则不会,这会为开发带来一些麻烦,但下面会给出解决方案。
4. 浏览器兼容
特性 Chrome Firefox (Gecko) Internet Explorer Opera Safari
pushState, replaceState 5 4.0 (2.0) 10 11.50 5.0
history.state 18 4.0 (2.0) 10 11.50 6.0
三. 使用 History 的新特性增强 Ajax 体验
这里说的增强体验,实质就是要解决文章开头提到的两个问题 —— Ajax 翻页时不会修改 URL 以及前进、后退功能无效。
看了上面的方法,相信大家已经能想到基本的解决思路,无非就是在 Ajax 需求产生时 pushState 一条记录到历史记录中,然后在 popstate 的回调函数中作出相应的处理。当然,要想实际获得比较好的体验,要做的内容还是比较复杂的。下面以 Ajax 分页为例,提供一个完整思路:
每次使用 Ajax 获取新页面后,使用 pushState() 把新页面的 url 压入历史记录栈顶。
监听 popstate 事件,当用户前进或后退时在回调中根据前进或后退后的 URL 重新使用 Ajax 获取页面内容,实现 Ajax 前进与后退。
Chrome 在打开页面以及刷新页面后会产生 popstate 事件,导致多余的 Ajax 请求,因此需要 popstate 中利用 url 作为判断用户是否在点击前进或后退功能,抑或是打开、刷新页面,若为前进或后退才触发 Ajax 获取相应内容。
基本代码
代码如下 | 复制代码 |
// 定义 Ajax 获取分页方法,这里不列出具体内容 function turnPage(url){ // ... } // 定义 popstate 的回调 if( history.pushState ){ // 判断浏览器是否支持 pushState // currentState 用于记录 popstate 产生前的页面 url,页面加载时初始化 currentState 为当前链接 var currentState = window.location.href; window.addEventListener('popstate', function(event){ // _currentUrl 用于记录 popstate 产生时的页面 url var _currentUrl = window.location.href; /* 判断 currentState 和 _currentUrl 是否相同, * 若相同则表明这个 popstate 产生前后页面 url 没有变化, * 即页面是第一次加载页面或者被刷新,无需触发新的 Ajax 请求, * 若不同则表明 url 已改变,这是触发 Ajax 获取内容 */ if( currentState != _currentUrl ){ console.log('获取新分页'); turnPage(_currentUrl); // 根据当前 url 重新获取分页内容 currentState = _currentUrl; // 更新 currentState } }); } // 绑定点击分页产生 Ajax 请求 // 点击分页链接,获取 url 为 currentLink,产生一次 Ajax 请求 turnPage(currentLink); // 调用 pushState() 压入新记录 if( history.pushState ){ // 判断浏览器是否支持 pushState // 往历史记录中压入新记录,浏览器的地址栏上的 url 同时会被修改为新 url history.pushState(null, document.title, currentLink); currentState = window.location.href; // 分页时更新 currentState 为当前 url } |
本站已经根据上面的思路进行了 Ajax 分页的增强,具体的情况如下:
打开页面,url 显示为首页的 url
点击第二页链接,触发 Ajax 分页,在 Ajax 中调用 pushState() ,可以看到,url 已经被修改为第二页的 url
点击后退按钮,url 地址变为第一页,再在 popstate 的回调中触发 Ajax 获取第一页的内容。
这样,就做到了既吸收了局部刷新页面的好处同时保留传统的 url 导航体验。而对于不支持 pushState 的浏览器,可以使用渐进增强设计忽略这些浏览器,毕竟这是一个增强体验的功能;当然如果必须照顾那些浏览器,也可以采用 hash 的方案代替。