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';
(либо напрямую через ссылку
My Link
).

Внешне это выглядит как обычная страница, но мы хотим добиться, чтобы обновлялась только внутренняя часть страницы без полной перезагрузки.

Для начала нарисуем SiteMesh layout примерно такого вида:

grails-app/views/layouts/main.gsp
<g:layoutTitle default="Grails" />
/>
/>
<g:layoutHead />
<g:javascript library="jquery"/>
<g:javascript library="application" />

...
%{-- Навигационные ссылки --}%
Рецепты

/>

Где купить
/>

Отзывы
/>



%{-- Тело страницы --}%
<g:layoutBody />

...



Здесь пока никаких сюрпризов. Как видим, ссылки ничем не отличаются от обычных 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" %>
/>

Чего и как приготовить поесть


  • ${receipt.encodeAsHTML()}




Теперь повесим контроллер на наши ссылки /do/*:

grails-app/conf/UrlMappings.groovy
class UrlMappings {
static mappings = {
"/do/$action?/$id?" {
controller = 'doSomething'
}
}
}


Спасибо за внимание.