История и навигация в HTML5. Методы pushState и replaceState

В html5 появились возможности навигации без перезагрузки страницы, при этом корректно отрабатывается функции "назад" и "вперед" браузера.

Для этого используется следующий функционал:

history.pushState(obj, title, Url);
history.replaceState(obj, title, Url);
и обработчик
onpopstate

Приведенный здесь скрипт имеет несколько отличительных особенностей от множества примеров в интернете:

  • скрипт корректно отрабатывает внутренние ссылки: <a name=""></a>;
  • изменяется тег base. Иначе после загрузки контента все ссылки будут указывать "не туда";
  • подгружается заголовок страницы;
  • учитывается, нажата ли с кликом мыши клавиша Ctrl или Shift для отмены загрузки через обработчик;
  • отрабатывается работа через hash, при отстутствии поддержки html5 браузером.

Объект, в котором будет меняться содержимое:

<div id="main"></div>

Серверная часть должна предусматривать обработку дополнительного GET-параметра ajax=1, при котором возвращается только тело страницы, без обрамления. Т.е. только то, что нужно загрузить в DIV-блок.

Для смены заголовка страницы можно дополнительно передать:

<title>ЗАГОЛОВОК</title>

Если в теле странцы есть элемент Base, его содержимое при подгрузке страницы будет перезаписано.

Для загрузки содержимого страницы (или части страницы) без перезагрузки используется следующий скрипт:

var oldUrl=document.location;
var MainUrl='';

function LoadMain(e0){
 e=e0||window.event;
 if(e){if(e.ctrlKey||e.shiftKey)return true;} // если нажата Ctrl или Shift, то загружать в отдельном окне

 if(e0 && e0.stopPropagation){e0.stopPropagation();e0.preventDefault();}       // для DOM-совместимых браузеров
 else window.event.cancelBubble=true; //для IE

 url=getEventTarget(e0);
 if(url.nodeName!='A'&&url.parentNode)url=url.parentNode;

 if(url.href)url=url.href;
 LoadMainUrl(url);
 if(history.pushState)history.pushState(null, null, MainUrl);
 else window.location.hash='#'+MainUrl;
 return false;
}


function ajaxLoad(obj,url,defMessage,post,callback){
  var ajaxObj;
  if(typeof(obj)!="object")obj=document.getElementById(obj);
  if(defMessage)obj.innerHTML=defMessage;
  if(window.XMLHttpRequest){
      ajaxObj = new XMLHttpRequest();
  } else if(window.ActiveXObject){
      ajaxObj = new ActiveXObject("Microsoft.XMLHTTP");
  } else {
      return;
  }
  ajaxObj.open ((post?'POST':'GET'), url);
  if(post&&ajaxObj.setRequestHeader){
    ajaxObj.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8;");
  }
  ajaxObj.setRequestHeader("Referer", window.location.href);
  ajaxObj.onreadystatechange = ajaxCallBack(obj,ajaxObj,(callback?callback:null));
  ajaxObj.send(post);
  return false;
}
try{
setTimeout( function() {
window.addEventListener("popstate", function(e) {
 MainUrl=e.location || document.location;
 if(oldUrl.pathname==MainUrl.pathname && oldUrl.hash.substring(1,1)!='/'){
    /*alert(oldUrl.pathname+'|'+MainUrl.pathname+'|'+oldUrl.hash);*/
    return;}
 LoadMainUrl(MainUrl.href);
}, false);
}, 900 );
}catch(e){};

function LoadMainUrl(url){
 e=window.location.hostname;
 MainUrl=url;
 i=url.indexOf(e); if(i>0)url=url.substring(i+e.length);
 if(url.substring(0,1)!='/')return;
 i=url.indexOf('#');
 if(i>0)url=url.substring(0,i)+(url.indexOf('?')>=0?'&':'?')+'ajax=1'+url.substring(i);
 else url=url+(url.indexOf('?')>=0?'&':'?')+'ajax=1';
 ajaxLoad('main',url,'Загрузка...')
}

function updateObj(obj, data){
   if(typeof(obj)!="object")obj=document.getElementById(obj); if(!obj)return;
   if(obj.id=='main'){
   var re1=new RegExp ("<title>([^<]+)</title>","i"); text=re1.exec(data);
   if(text!=null){t=text[1]; document.title=t;
    data=data.replace(re1, "");
    }
   obj.innerHTML=data;
   el=document.getElementsByTagName('base')[0];
   if(!el){el=document.createElement("base");
       document.getElementsByTagName('head')[0].appendChild(el);}
   el.setAttribute('href', MainUrl);
   oldUrl=MainUrl;
   window.scroll(0,0);
   return;
   }
   obj.innerHTML = data;
}
function ajaxCallBack(obj, ajaxObj, callback){
return function(){
    if(ajaxObj.readyState==4){
       if(callback) if(!callback(obj,ajaxObj))return;
       if (ajaxObj.status==200){
            updateObj(obj, ajaxObj.responseText);
        }
       else updateObj(obj, ajaxObj.status+' '+ajaxObj.statusText);
    }
;
}}

function getEventTarget(e) {
  var e = e || window.event;
  var target=e.target || e.srcElement;
  if(typeof target == "undefined")return e; // передали this, а не event
  if (target.nodeType==3) target=target.parentNode;// боремся с Safari
  return target;
}

Назначение обработчика загрузки на ссылку:

<a href="/html/" onclick="return LoadMain(this)">Html</a>

Если Вы хотите на все ссылки установить загрузку через свой обработчик, используйте следующий код:

addEvent(examples, 'click', function (event) {
  event.preventDefault();
  if (event.target.nodeName == 'A') {
    LoadMain(event);
    /*title = event.target.innerHTML;
    data[title].url = event.target.getAttribute('href');
    history.pushState(data[title], title, event.target.href);*/
  }
});

Еще один пример использования сохранения адреса:

u="http://"+document.location.host+document.location.pathname+document.location.search;
    hash=document.location.hash; // после #
    console.log(u,hash);
    if(u.indexOf('?')>=0)u=(u+'&').replace(/url=(.*?)&/gi,"").replace(/&&/gi,"&");
    var i=u.substr(u.length-1,1);
    if(i=='?'||i=='&')u=u.substr(0,u.length-1);
    u=u+(u.indexOf('?')>=0?'&':'?')+"url="+encodeURIComponent(url)+hash;
    MainUrl=u;
    if(history.pushState)history.pushState(null, null, MainUrl);

Интерфейс History html5

Приведено описание наболее важных компонентов.

interface History {
    void pushState(in any data, in DOMString title, in optional DOMString url);
    void replaceState(in any data, in DOMString title, in optional DOMString url);
};

interface HTMLBodyElement : HTMLElement {
    attribute Function onpopstate;
};

Эмулятор pushState и onpopstate

// сохранение ссылки в location.hash
function _pushState(state) {
    window.location.hash = state;
}

// обнаружение изменения в location.hash
var lastState;
window.setTimeout(function() {
        var state = window.location.hash;
        if (lastState != state) {
            lastState = state;
            LoadMain(state);
        }
    }, 100);

.