PJAX和html5 history api

什么是PJAX

PJAX其实是一组技术的简称,即HTML5的pushState+ajax,实现的效果是无刷新的改变页面某个部分的内容(ajax),同时地址栏URL又能随之改变(HTML5 push State)

访问github的时候当查看某个文件的源码的时候发现其实整个页面没有刷新而URL改变了其实也是用到了PJAX,还有如网易云音乐等

为什么会产生PJAX

  • 为了更好的用户体验,URL能改变同时页面又能局部刷新,即所谓的单页面应用single page application
  • 为了能让ajax被搜索引擎抓取,有利于SEO

如何实现PJAX

其实就是利用html5的history api和普通的ajax实现了

history api

Window.history是一个只读属性,用来获取History 对象的引用,History 对象提供了操作浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中通过框架加载的页面)的接口

history.forward()    //等同于浏览器的前进按钮  
history.back();     // 等同于点击浏览器的回退按钮  
history.go(-1);     //移动到指定的历史记录点,-1等同于history.back();1等同于history.forward();类似的,传递参数“2”,你就可以向前移动2页  

HTML5引进了history.pushState()方法和history.replaceState()方法,它们允许你逐条地添加和修改历史记录条目。这些方法可以协同window.onpopstate事件一起工作

使用 history.pushState() 会改变 referrer 的值,而在你调用方法后创建的 XMLHttpRequest 对象会在 HTTP 请求头中使用这个值。referrer的值则是创建 XMLHttpRequest 对象时所处的窗口的URL

假设在http://shaynegui.com/foo.html 页面执行以下代码

var stateObj = { foo: "bar" };  
history.pushState(stateObj, "page 2", "bar.html");  

这将让浏览器的地址栏显示http://shaynegui.com/bar.html 但不会加载bar.html页面也不会检查bar.html是否存在

假设现在用户导航到了http://google.com 然后点击了后退按钮,此时,地址栏将会显示http://shaynegui.com/bar.html 并且页面会触发popstate事件,该事件中的状态对象(state object)包含stateObj的一个拷贝。该页面看起来像foo.html,尽管页面内容可能在popstate事件中被修改。

如果我们再次点击后退按钮,URL将变回http://shaynegui.com/foo.html ,文档将触发另一个popstate事件,这次的状态对象为null。回退同样不会改变文档内容

pushState()方法

pushState()有三个参数:一个状态对象、一个标题(现在会被忽略),一个可选的URL地址

  • 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。任何可序列化的对象都可以被当做状态对象。

  • 标题(title) — FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。

  • 地址(URL) — 新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。

某种意义上,调用pushState()有点类似于设置window.location='#foo',它们都会在当前文档内创建和激活新的历史记录条目。但pushState()有自己的优势

  • 新的URL可以是任意的同源URL,与此相反,使用window.location方法时,只有仅修改 hash 才能保证停留在相同的document中
  • 根据个人需要来决定是否修改URL。相反,设置window.location='#foo',只有在当前hash值不是foo时才创建一条新历史记录
  • 你可以在新的历史记录条目中添加抽象数据。如果使用基于hash的方法,你只能把相关数据转码成一个很短的字符串

注意pushState()方法永远不会触发hashchange事件,即便新的地址只变更了hash,后面会说道hashchange

replaceState()方法

history.replaceState()操作类似于history.pushState(),不同之处在于replaceState()方法会修改当前历史记录条目而并非创建新的条目;当你为了响应用户的某些操作,而要更新当前历史记录条目的状态对象或URL时,使用replaceState()方法会特别合适

popstate事件

每当处于激活状态的历史记录条目发生变化时,popstate事件就会在对应window对象上触发. 如果当前处于激活状态的历史记录条目是由history.pushState()方法创建,或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝拷贝

调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在其他浏览器操作时触发, 比如点击后退按钮(或者在JavaScript中调用history.back()方法).

当网页加载时,各浏览器对popstate事件是否触发有不同的表现,Chrome 和 Safari会触发popstate事件, 而Firefox不会

假如当前网页地址为http://example.com/example.html, 则运行下述代码后

window.onpopstate = function(event) {  
  alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//绑定事件处理函数. 
history.pushState({page: 1}, "title 1", "?page=1");    //添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1  
history.pushState({page: 2}, "title 2", "?page=2");    //添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2  
history.replaceState({page: 3}, "title 3", "?page=3"); //修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为2  
history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"  
history.back(); // 弹出 "location: http://example.com/example.html, state: null  
history.go(2);  // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}  

即便进入了那些非pushState和replaceState方法作用过的(比如http://example.com/example.html) 没有state对象关联的那些网页, popstate事件也仍然会被触发

读取当前状态

在页面加载时,可能会包含一个非空的状态对象。这种情况是会发生的,例如,如果页面中使用pushState()或replaceState()方法设置了一个状态对象,然后用户重启了浏览器。当页面重新加载时,页面会触发onload事件,但不会触发popstate事件。但是,如果你读取 history.state 属性,你会得到一个与 popstate 事件触发时得到的一样的状态对象

你可以直接读取当前历史记录条目的状态,而不需要等待popstate事件

var currentState = history.state  

兼容性列表如下

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
replaceState, pushState 5 4.0 (2.0) 10 11.50 5.0
history.state 18 4.0 (2.0) 10 11.50 6.0

对于不支持HTML5 history api如何实现呢

可以优雅降级为url中hash的内容来实现(即把需要获取的新页面的URL当作参数写到hash后面),如:http://www.shaynegui.com/#/foo.html 然后监听hashchange事件

hashchange事件会在页面URL中的片段标识符(第一个#号开始到末尾的所有字符,包括#号)发生改变时触发.

hashchange 事件对象有下面两个属性

  • newURL 当前页面新的URL
  • oldURL 当前页面旧的URL
window.onhashchange = function(event){  
    alert(event.newURL);
    alert(event.oldURL);
}

此事件的兼容性列表如下

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 5.0 3.6 (1.9.2) 8.0 10.6 5.0

那么对于ie8一下的版本,因为无法监听hashchange事件只能通过定时器轮询来知道hash是否改变

(function(window) {

  // 如果浏览器原生支持该事件,则退出  
if ( "onhashchange" in window.document.body ) { return; }

  var location = window.location,
    oldURL = location.href,
    oldHash = location.hash;

  // 每隔100ms检测一下location.hash是否发生变化
  setInterval(function() {
    var newURL = location.href,
      newHash = location.hash;

    // 如果hash发生了变化,且绑定了处理函数...
    if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
      // execute the handler
      window.onhashchange({
        type: "hashchange",
        oldURL: oldURL,
        newURL: newURL
      });

      oldURL = newURL;
      oldHash = newHash;
    }
  }, 100);

})(window);

因此对于不支持history api和hashchange事件的IE8以下的浏览器,可以使用轮询url的hash部分实现一样的PJAX效果

参考文档

MDN window.onhashchange

MDN window.history

MDN Manipulating the browser history

MDN window.onpopstate