Pull to refresh

Grails, jQuery, AJAX: делаем anchor-навигацию. Часть 2, финальная

Groovy & Grails *

Полная и неполная страницы


Продолжаем разговор про anchor-навигацию. Наша цель — сделать рабочее приложение на Grails.

Есть одна тонкость. Очень хочется, чтобы страница могла быть показана как в полном варианте (с шапкой, навигацией и т.п.), так и в сокращенном (для AJAX-вызовов). Однако набрав /my-app/do/receipts, получим полный вариант. Теперь это выглядит так:

Oops! Надо как-то различать ситуацию, когда страница является главной и когда она внутренняя. Для этого напишем небольшой фильтр:

grails-app/conf/PartialPageLoadFilters.groovy
    def filters = {
        partial(controller: "*", action: "*") {
            before = {
                // Это AJAX-запрос?
                if (request.xhr) {
                    // Нужно показывать как внутреннюю страницу.
                    request.partialPage = true
                }
                true
            }
        }
    }

Отмечу использование волшебного атрибута request.xhr, который предоставляет Grails. Его присутствие означает, что текущий запрос вызван через AJAX. Большинство браузеров сообщает об этом серверу через специальный HTTP-Header. Выше я просто объявил все AJAX-запросы «внутренними», в реальном приложении ситуация может быть сложнее.

Теперь я могу везде использовать флаг request.partialPage. Можно сделать отдельный layout для внутренних страниц, но я предпочел просто тупо сделать вот такие вставки в основной layout:

grails-app/views/layouts/main.gsp
%{-- Частичный вариант --}%
<g:if test="${request.partialPage}">
    <g:layoutBody />
</g:if>
<g:else>
%{-- Полный вариант страницы --}%
<html>
    <head>
     ...
    </head>
    <body>
        ...
        <div id="pageContent">
            <g:layoutBody />
        </div>
        ...
    </body>
</html>
</g:else>

Мы добились полной прозрачности (для контроллера и GSP) в том, какой из вариантов страницы показывать.

Закладки и история


Итак, имеем три экрана, переключаемые через AJAX:
/my-app/#do/receipts /my-app/#do/buy /my-app/#do/feedback

Все очень здорово, но, как выясняется, наши «динамические» ссылки нельзя поместить в закладки. Дело в том, что они содержат anchor-хвост, который на сервер не передается. Поэтому сервер при загрузке такого URL не знает, какую из внутренних страниц показывать.

Про anchor знает только JavaScript. Напрашивается такое решение: сначала загрузить базовую страницу с JavaScript, затем запустить код, определяющий текущий anchor и загружающий внутреннюю часть при помощи AJAX. Особо не заморачиваясь, я написал такой код:

web-app/js/application.js
$.ready(function() {
    $('#pageContent').html('Загружаем...')
        .load(buildURL(document.location.hash), function() {
            // Перерисовываем ссылки
            updateNavLinks();
        });
});

Этот код лишь демонстрирует идею. При желании этот код можно допилить до более красивого состояния, однако суть не изменится: сначала придется загрузить базовую обертку, затем пользователю придется ждать загрузки внутренней части. Например, Facebook обычно вообще ничего не показывает, пока внутренняя часть не загрузится — дело вкуса.

А как быть с историей? При переходах Back/Forward наше событие $.ready не сработает. Такие переходы браузер считает «перескоком» от одного якоря к другому, т.е. просто пролистыванием страницы. Никаких уникально идентифицируемых JavaScript-событий при этом не возникает. Что делать?

Один из способов решить это (сам по себе довольно брутальный) — периодически проверять свойство document.location.hash на предмет изменений. Если вдруг текущий якорь изменился, нужно перегрузить внутреннюю часть страницы.

function checkLocalState() {
    if (document.location.hash && document.location.hash != currentState) {
        currentState = document.location.hash;

        $('#pageContent').html('Ссылка изменилась, загружаем...')
            .load(buildURL(currentState), function() {
                // Перерисовываем ссылки
                updateNavLinks();
        });
    }
}

Теперь проверяем изменение якоря каждые 500 миллисекунд:

$.ready(function() { setInterval(checkLocalState, 500); });

Теперь наше приложение будет реагировать на переходы Back/Forward, но с задержкой в полсекунды. Такую задержку (иногда раздражающую пользователя) можно увидеть на многих крупных сайтах, использующих сходную схему навигации. Если уменьшить интервал, фоновый JavaScript будет есть больше ресурсов. Если интервал увеличить, возрастет время реакции. В общем, за все надо платить.

Резюме


Мы создали Grails + jQuery приложение с AJAX-навигацией, которое:
  1. Перезагружает только внутреннее содержимое страницы без перегрузки всей страницы.
  2. Правильно сохраняет состояние страницы в адресной строке, т.е. годится для закладок и передачи ссылки знакомым.
  3. Правильно реагирует на переходы Back/Forward в браузере.
  4. Позволяет разрабатывать серверный код «по-старому», не заморачиваясь новыми правилами игры, т.к. вся логика загрузки страниц сделана прозрачным (для контроллеров и GSP-страниц) образом.
  5. Ссылки можно открывать в новом окне и они будут работать корректно.
  6. Отмечу, что и ссылки в нашем приложении ничем не отличаются от обычных ссылок, за исключением решетки(#) в начале URL! Ведь мы заботливо унесли всю логику работы ссылок в jQuery-код.

Конечно, в реальном приложении приведенный здесь код придется улучшить, в частности, более аккуратно учитывать ситуации отсутствия якоря, использовать более сложную схему построения ссылок и т.п. Однако надеюсь, что он успешно демонстрирует идею и решает большую часть проблем с anchor-навигацией.

Ссылки:
  1. http://yensdesign.com/2008/11/creating-ajax-websites-based-on-anchor-navigation/
  2. GWT Tutorial – Managing History and Hyperlinks
Tags:
Hubs:
Total votes 6: ↑6 and ↓0 +6
Views 3.3K
Comments Comments 1