Архитектура AJAX-сайтов
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();.
565
просмотров
Комментарии
Daniil Kirichenko
Автор блога
Потому что фреймворки тащат за собой тонны бесполезного кода, зачастую увеличивая время работы и усложняя тонкую настройку, например, кэширование популярных страниц в Local Storage, как сделано у меня. Тут же одна коротенькая, понятная слёту функция, отлично выполняющая свою работу.
Не вижу ни одной причины заменять её на фреймворк.
Ryan Mathews
Для этого существуют фреймворки, которые завязаны на SPA.

Не имею ни малейшего понятия, почему ты используешь такого рода костыли.
Чтобы оставить комментарий, авторизуйтесь. Это займёт всего 10 секунд.
Вконтакте