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

Oops! Надо как-то различать ситуацию, когда страница является главной и когда она внутренняя. Для этого напишем небольшой фильтр:
grails-app/conf/PartialPageLoadFilters.groovydef 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-навигацией, которое:
- Перезагружает только внутреннее содержимое страницы без перегрузки всей страницы.
- Правильно сохраняет состояние страницы в адресной строке, т.е. годится для закладок и передачи ссылки знакомым.
- Правильно реагирует на переходы Back/Forward в браузере.
- Позволяет разрабатывать серверный код «по-старому», не заморачиваясь новыми правилами игры, т.к. вся логика загрузки страниц сделана прозрачным (для контроллеров и GSP-страниц) образом.
- Ссылки можно открывать в новом окне и они будут работать корректно.
- Отмечу, что и ссылки в нашем приложении ничем не отличаются от обычных ссылок, за исключением решетки(#) в начале URL! Ведь мы заботливо унесли всю логику работы ссылок в jQuery-код.
Конечно, в реальном приложении приведенный здесь код придется улучшить, в частности, более аккуратно учитывать ситуации отсутствия якоря, использовать более сложную схему построения ссылок и т.п. Однако надеюсь, что он успешно демонстрирует идею и решает большую часть проблем с anchor-навигацией.
Ссылки:



