C появлением возможности управлять историей браузера через объект window.history (а не только переходить по ней) и развитием AJAX было логичным, что появились сайты, которые подгружают контент своих страниц без перезагрузки самой страницы. Одним из первых крупных сайтов, который перешёл на AJAX, был Вконтакте в далёком 2010 году. Для него этот шаг, как мне кажется, был отчасти вынужденным — при стандартной архитектуре музыку приходилось слушать в отдельной вкладке, а фотографии пролистывать с перезагрузкой всей страницы.
Мой недавно запущенный проект Monopoly One так же построен на AJAX. Далее я расскажу вам, как правильно делать AJAX-сайты и не оказаться похороненным под грудой ошибок (вроде того, что вам придётся менять код на сервере, но это неправда).
Итак, моя прелесть
Сначала немного о том, как, собственно, менять адрес в строке браузера. У вас есть два варианта — либо манипулировать location.hash, либо использоваться современный window.history.pushState. Лично я не поддерживаю изменение хэша, так как манипуляция непосредственно историей поддерживается всеми современными браузерами.
Делается это просто:
function openPage(url){
window.history.pushState({}, 'Моя новая страница', url);
}
Первый параметр — объект с данными, которые будут переданы в событие onpopstate, когда пользователь решит вернуться на этот объект истории при помощи кнопки "вперёд" или "назад". Его я оставляю пустым, так как он не несёт полезной для меня нагрузки. Второй параметр — это заголовок страницы, который сохранится в истории браузера. Помните, что тут не будет изменён заголовок самой страницы, только объекта в истории браузера! Третий параметр — собственно URL самой страницы, причём протокол, домен и порт должны совпадать с текущими (само собой).
Итак, вы знаете, как поменять URL страницы. Теперь эту функцию надо как-то вызывать при клике по ссылкам. Это легко:
$(document).on('click', 'a', function(event){
if(this.hostname === location.hostname){
openPage(this.href);
event.preventDefault();
return false;
}
});
Обратите внимание, что устанавливать обработчики глобальных событий вам теперь придётся на $(document), так как при обычном $('a') обработчики будут установлены только на те ссылки, которые есть в DOM сейчас, а после обновления контента страницы новые ссылки не будут вызывать этот обработчик. И да, мы обрабатываем только те ссылки, домен которых равен текущему (можно добавить в условие проверку протокола и порта, но вряд ли вам это понадобится). Так же вам понадобится проверять клик колёсиком мыши или клик с зажатым Ctrl (или Cmd для OS X), чтобы открывать страницу в новой вкладке, как того и ожидает пользователь, но это сделайте уже сами.
Отныне все ссылки вроде <a href="/path1">кликни меня</a> не будут перезагружать страницу и будут менять контент адресной строки браузера. Но пока не контент страницы.
Отдача страниц сервером
Некоторые (особенно старые) уроки по AJAX-сайтам говорят, что вам надо будет делать AJAX-запрос к серверу (правда), но добавлять GET-параметр вроде ?ajax=true, чтобы сервер отдавал только содержимое <body> (это наглая ложь и ошибка). Я делал подобное ещё в 2011 году и там на самом деле было что-то вроде такого PHP (прости господи) кода:
<?
$ajax = @$_GET["ajax"] === 'true';
if(false === $ajax){
?>
<html><head>
<title>Заголовок</title>
</head><body>
<? } ?>
<div>Hello, world!</div>
<img src="/img/hello.png">
<? if(false === $ajax){ ?>
</body></html>
<? } ?>
Ну а потом скрипт на клиенте просто менял контент тэга <body>.
Почему это плохо?
Если для вас это не очевидно, то сейчас расскажу. Во-первых, вы не получаете тэг <head> при AJAX-запросе, а это значит, что вы не можете в процессе подключить скрипты и стили. Значит, вам придётся все нужные сайту скрипты и стили загружать при первом запросе страницы, что может очень сильно увеличить время загрузки страницы. Во-вторых, вы не получаете тег <title>, и это значит, что вам придётся в <body> каждой страницы иметь что-то вроде <script>document.title="Заголовок второй страницы"</script>, что не делает код красивее и вообще выглядит как грязный костыль. В-третьих, вы перекладываете работу по генерации нужной страницы на сервер, но зачем? Причин можно найти ещё несколько, это только те, которые я вспомнил за пару секунд.
А как правильно-то?
Так зачем осложнять себе миграцию на новую архитектуру? Оставьте сервер в покое и переложите задачу по правильному встраиванию нового контента в страницу клиентскому скрипту, у вас и так будет несколько других проблем по миграции (они чуть ниже будут описаны).
Клиентский скрипт
Итак, сервер страницу не меняет никак. AJAX-запрос получает её целиком в первозданном виде. Его задачи таковы: распарсить страницу; найти тег <body> и вставить его контент в страницу; найти тег <title> и установить новый заголовок страницы; пройтись по скриптам и стилям в теге <head> и вставить в имеющийся тег <head> (который был получен при первой загрузке страницы) все те скрипты и стили, которых там ещё нет.
Вот вам примерный код:
function openPage(url){
$.ajax({
url: url,
// если не указать dataType как text, то jQuery будет парсить html
// и в итоге парсинг будет проходить дважды
// что довольно нагрузочно для браузера
dataType: 'text',
complete: function(data){
var html = rdata.responseText; // достаём текст ответа
// заменяем теги html, head и body в тексте на аналоги
// так как браузеры не разрешают парситься этим тегам
html = html
.replace(/<html>/, '<p-html>')
.replace(/<\/html>/, '<\/p-html>')
.replace(/<head>/, '<p-head>')
.replace(/<\/head>/, '</p-head>')
.replace(/<body[^>]{0,}>/, '<p-body>')
.replace(/<\/body>/, '</p-body>');
// парсим html
html = $(html);
// обновляем заголвок страницы
document.title = html.find('p-head > title').html();
// обновляем URL
window.history.pushState({}, document.title, url);
// перебираем скрипты и стили в полученной странице
html.find('p-head').children().each(function(){
var el = $(this),
tagName = this.tagName.toLowerCase(),
selector = 'head ' + tagName,
el_new = $('<' + tagName + '>');
switch(tagName){
case 'link':
selector += '[rel="' + el.attr('rel') + '"][href="' + el.attr('href') + '"]';
el_new.attr({
rel: el.attr('rel'),
href: el.attr('href')
});
break;
case 'script':
if(el.attr('src') === undefined) return;
selector += '[src="' + el.attr('src') + '"]';
el_new.attr('src', el.attr('src'));
break;
default:
return;
}
if($(selector).size() === 0){
$('head').append(el_new);
}
});
// обновляем содержимое body
$('body')
.html( html.find('p-body > *') )
.scrollTop(0);
}
});
}
Комментарии находятся в коде, так что просто читайте. А теперь несколько вопросов из зала.
Почему используется complete, а не success?
Потому что сервер может вернуть код 404 (или 500, или любой другой) в виде страницы с ошибкой, а вам нужно эты страницу всё равно показать. А ошибки вроде таймаута обрабатывайте сами, это не задача данной статьи.
Что будет с обработчиками событий?
Они пропадут, если были поставлены непосредственно на элемент, а не на $(document). Да, вам придётся быть внимательнее с обработчиками событий элементов.
А что там с setInterval?
Функции setInterval и setTimeout не будут никак изменены и продолжат работу. Если вам это не нужно (например, интервалы для листания карусели только на главной странице), то ставьте их через функцию-обёртку, сохраняйте id таймера и очищайте его после (или до) обновления содержимого <body>. Точно так же поступайте с AJAX-запросами (например, к API сервиса), выполнение коллбэка которых после смены страницы не имеет смысла — просто делайте ajaxObject.abort();.
Не вижу ни одной причины заменять её на фреймворк.
Не имею ни малейшего понятия, почему ты используешь такого рода костыли.