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.groovyclass 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.groovyclass UrlMappings { static mappings = { "/do/$action?/$id?" { controller = 'doSomething' } } }
Однако этого недостаточно. Есть проблемы: нашу клиентскую и серверную часть надо доработать, о чем напишу в следующей части.