AJAX и все, все, все
В предыдущей серии мы делали простенькое Grails-приложение с использованием jQuery, а также решили для себя, что использовать jQuery в Grails можно и даже нужно. Обсудим более серьезные вещи, которые можно сделать с такой связкой.
Нетрудно заметить, что все больше сайтов используют AJAX и частичные обновления страниц, причем в невероятном количестве. В частности, «начиненные» AJAX ссылки могут использоваться для внутренней навигации по странице, переключения каких-то вкладок. Это хорошо тем, что
А) меньше данных нужно перегонять от сервера — только нужный кусок страницы и
Б) веб-страницы часто загружают просто гигантские CSS и JavaScript-файлы, которые при AJAX-обновлении можно повторно не загружать.
Итак, очень распространено построение приложений по сценарию: одна большая «стартовая» страница, загружающая весь JavaScript-код и CSS и более мелкие «внутренние» функциональные блоки, загружаемые через AJAX. С этим есть ряд проблем:
- В результате AJAX-действий внутреннее состояние страницы не отражено в адресной строке браузера.
- Как следствие, внутренние страницы не могут быть запомнены в закладки, нельзя «отправить ссылку другу».
- Не работает Back/Forward навигация в браузере, т.к. AJAX-ссылки не попадают в историю браузера.
Anchor-навигация
Дело в том, что на самом деле можно изменить адресную строку браузера без перезагрузки страницы, если менять только якорь (anchor), т.е. последнюю часть адресной строки, следующей за решеткой —
#
. Браузер воспринимает это переход внутри страницы, причем спокойно игнорирует ситуацию, когда нужного якоря на странице нет, просто обновляя адрес и историю. Это как раз нам и нужно. Если сохранять состояние страницы внутри якоря, тогда можно будет к нему вернуться через закладку и можно пользоваться Back/Forward переходами (!). При этом базовый URL страницы не изменится и перезагрузки страницы не произойдет.За примерами реализации подобного решения далеко ходить не нужно. Такая схема применяется в Facebook, Gmail, Google Picasa Web Albums, в значительном объеме это можно увидеть на odnoklassniki.ru. Библиотека Google Web Toolkit целиком базируется на anchor-навигации.
Скажем, в Gmail можно получить прямую ссылку на письмо, причем ссылка будет bookmarkable. Ссылка будет выглядеть примерно вот так:
mail.google.com/mail/?shva=1#inbox/12c5f14c01a5473c
Ежу ясно, что
12c5f14c01a5473c
— это какой-то внутренний ID письма.Пишем приложение
Подумаем на тему реализации такого подхода. Адресная строка меняется просто:
document.location.hash = '#myAnchor';
(либо напрямую через ссылку
<a href="#myAnchor">My Link</a>
).Начнем писать Grails-приложение с интригующим названием my-app с навигацией, целиком основанной на AJAX. У нашего приложения будет три вкладки:

Внешне это выглядит как обычная страница, но мы хотим добиться, чтобы обновлялась только внутренняя часть страницы без полной перезагрузки.
Для начала нарисуем SiteMesh layout примерно такого вида:
grails-app/views/layouts/main.gsp
<html>
<head>
<title><g:layoutTitle default="Grails" /></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="${resource(dir:'css',file:'main.css')}" />
<g:layoutHead />
<g:javascript library="jquery"/>
<g:javascript library="application" />
</head>
<body>
...
%{-- Навигационные ссылки --}%
<div class="navbar">
<div class="navitem">
<a href="#do/receipts" class="navlink">Рецепты</a>
<div class="spinner" />
</div>
<div class="navitem">
<a href="#do/buy" class="navlink">Где купить</a>
<div class="spinner" />
</div>
<div class="navitem">
<a href="#do/feedback" class="navlink">Отзывы</a>
<div class="spinner" />
</div>
</div>
%{-- Тело страницы --}%
<div id="pageContent">
<g:layoutBody />
</div>
...
</body>
</html>
Здесь пока никаких сюрпризов. Как видим, ссылки ничем не отличаются от обычных anchor-ссылок. Как же они работают? Для этого напишем такой код на jQuery:
web-app/js/application.js
// Здесь сохраняем текущее состояние страницы
var currentState = '';
function buildURL(anchor) {
return document.location.toString().substr(0, document.location.toString().indexOf('#')) +
anchor.substr(1);
}
function clickNavLink() {
// Уже там?
var href = $(this).attr('href');
// Игнорируем переход на уже загруженную страницу
if (href == currentState) {
return false;
}
if (document.location.hash != href) {
document.location.hash = href;
}
// Загружаем страницу
var link = this;
// Показываем индикатор загрузки
$(this).parent().find('.busy').show();
$(this).hide();
var targetURL = buildURL(href);
currentState = href; // сразу поменяем состояние, чтобы избежать повторных кликов
$.ajax({
context:$('#pageContent'),
url:targetURL,
dataType:'html',
method:'GET',
complete: function() {
// Отмечаем активную ссылку.
$(link).show();
updateNavLinks();
},
success: function(data) {
// Обновляем "динамическую" часть страницы.
$('#pageContent').html(data);
}
});
return true;
}
// Обновляем состояние ссылок, чтобы отметить активные/неактивные
function updateNavLinks() {
$('a.navlink').each(function(i) {
var href = $(this).attr('href');
$(this).parent().find('.busy').hide();
if (href == currentState) {
$(this).addClass('disabled');
} else {
$(this).removeClass('disabled');
}
});
}
// Финал. Вешаем события на навигационные ссылки.
jQuery(document).ready(function() {
$('a.navlink').each(function() { $(this).click(clickNavLink); });
});
Здесь все довольно просто: текущее состояние страницы мы храним в JavaScript-переменной currentState. Внутреннюю страницу при клике на ссылку загружаем через AJAX, результат AJAX-вызова сохраняем в div#pageContent. При этом URL загружаемой страницы формируется путем добавления anchor-пути к базовому адресу страницы, т.е.
/my-app/#do/receipts => /my-app/do/receipts
Это простое правило сразу помогает нам понять, что делает ссылка. Для того, чтобы обознать ссылку как «текущую», мы назначаем ей класс
disabled
. В CSS (который я приводить не буду) этот класс будет отображаться другим цветом, чтобы было видно, какая ссылка является текущей (visited).Серверная часть
Теперь хорошо бы сделать серверную начинку. Я написал простейший контроллер для обработки всех трех ссылок:
grails-app/controllers/DoSomethingController.groovy
class DoSomethingController {
def receipts = {
[receipts:['Курица с мандаринами', 'Пельмешки']]
}
def buy = {
[places:['Ларёк у метро', 'Чебуречная №1']]
}
def feedback = {
[feedback:['нравится','не нравится','не нравится, но ем!']]
}
}
и к нему сделал три простенькие страницы. Приведу только одну из них:
grails-app/views/doSomething/receipts.gsp
<%--
Список рецептов
--%>
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<meta name="layout" content="main" />
</head>
<body>
<h1>Чего и как приготовить поесть</h1>
<ul>
<g:each in="${receipts}" var="receipt">
<li>${receipt.encodeAsHTML()}</li>
</g:each>
</ul>
</body>
</html>
Теперь повесим контроллер на наши ссылки /do/*:
grails-app/conf/UrlMappings.groovy
class UrlMappings {
static mappings = {
"/do/$action?/$id?" {
controller = 'doSomething'
}
}
}
Однако этого недостаточно. Есть проблемы: нашу клиентскую и серверную часть надо доработать, о чем напишу в следующей части.