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