Пример использования History.js

В процессе переписывания движка блога я решил, что переход по ссылкам «Следующая / Предыдущая страница» должен происходить без перезагрузки страницы. Проще говоря, с помощью AJAX.

В принципе, при знании матчасти, сделать это проще простого: ловим onclick на ссылке, отправляем AJAX-запрос, получаем HTML с содержимым следующей страницы и вставляем его в соответствующий контейнер в нашем документе.

Однако, в этом решении есть один существенный недостаток. URL страницы при этом не меняется. Это значит, что не работает кнопка «Назад» в браузере, нельзя обновить страницу и получить тот же результат, нельзя сохранить текущую страницу в закладках или отправить её кому-нибудь по почте.

Некоторые извращенцы (Google) предлагают в качестве решения так называемый HashBang (#!), когда информация о текущем состоянии страницы сохранятеся в идентификаторе фрагмента. По такому принципу работает новый интерфейс Твиттера и некоторые другие популярные сайты.

Этот способ мне кажется ужасным костылём. Во-первых, это некрасиво :-) Во-вторых, wget 'http://twitter.com/#!/a_fedoseev' скачает главную страницу Твиттера, а не мои твиты. Более развёрнуто HashBang ругают на Хабре, см. Ломаем web c '#!' (hash-bang).

Хорошая новость заключается в том, что можно получить желаемый результат очень элегантным и простым способом — с помощью History.js. Это скрипт, который позволяет управлять состоянием страницы, используя HTML5 History API (в тех браузерах, которые его поддерживают). При этом состояние страницы сохраняетя в виде нормального URL и всё работает так, как будто вы на самом деле переходите с одной страницы на другую.

В старых браузерах и IE оно тоже может работать, но при этом используюся хаки с хешем в URL. Меня старые браузеры не волнуют, поэтому для них я просто отключаю History.js.

Теперь перейдём к делу. Я покажу и расскажу, каким образом реализовано перелистывание страниц у меня в блоге.

Упрощённая структуры страницы блога у меня выглядит так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<head>
    <title>Страница 2</title>
    ...
</head>
<body>
    ...
    <div class="page">
        <div class="entries">
            <article>
                <h1>...</h1>
            </article>
            ...
        </div>
        <div class="page-links">
            <a href="/blog/page/1" class="prev">Предыдущая страница</a>
            <a href="/blog/page/3" class="next">Следующая страица</a>
        </div>
    </div>
    ...
</body>
</html>

Думаю, что тут всё очевидно. При переходе по ссылке «Слудующая страница» попадаем на следующую страницу с URL /blog/page/3. Ссылка «Предыдущая страница» работает аналогично.

Вот как выглядит упрощённый Javascript код для переворачивания страницы, если использовать jQuery.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
$(function () {
    /*  Проверяем, поддерживается ли History.js браузером.
    Если нет, то ничего не делаем, пусть всё работает
    по-старинке. */
    if (!History.enabled) {
        return;
    }

    /*  Инициализируем контейнер для записей */
    var $entries = $("div.entries");

    /* Вешаем обработчики onlick на ссылки */
    var $page_links = $("div.page-links"),
        $prev_link = $page_links.find("a.prev"),
        $next_link = $page_links.find("a.next");
    $page_links.delegate("a", "click", function (e) {
        e.preventDefault();
        var url = $(this).attr("href");

        /*  Удаляем текущие записи из контейнера */
        $entries.empty();

        /*  Показываем индикатор загрузки */
        $entries.addClass("loading");

        /*  И сообщаем History.js об изменении состояния страницы
        В качестве первого агрумента можно передать произвольный объект
        с дополнительными данными, которые можно извлечь в обработчике
        изменения состояния, описанном ниже.
        В нашем случае это будет пустой объект. */
        History.pushState({}, null, url);
    });

    /*  Готовим обработчик изменения состояния страницы */
    History.Adapter.bind(window, "statechange", function () {
        /*  Получаем информацию о состоянии страницы */
        var state = History.getState();

        /* Получаем URL нового состояния. Это URL, который мы передали
        в .pushState() */
        var url = state.url;

        /*  Тут можно извлечь дополнительные данные, о которых шла речь выше.
        Например, так: var data = state.data; */

        /*  Отправляем AJAX-запрос на сервер.
        В качестве ответа мы ожидаем JSON-объект следующего формата:
        {entries: "<article><h1>...</h1>...</article> <article>...",
         title: "Страница 3",
         next_url: "/blog/page/4",
         prev_url: "/blog/page/2" }
        Каким образом будет сформирован этот ответ, зависит только от вас. */
        $.getJSON(url, function (response) {

            /*  Обновляем заголовок страницы */
            $("title").text(response.title);

            /*  Обновляем ссылки на предыдущую и следующую страницы */
            $prev_link.attr("href", response.prev_url);
            $next_link.attr("href", response.next_url);

            /*  И, наконец, показываем новый блок записей */
            $entries.removeClass("loading").html(response.entries);
        });
    });
});

Приведённый пример далёк от совершенства. Прежде всего, надо правильно обрабатывать случаи, когда дошли до самой последней или, наоборот, самой первой страницы. В этом случае нужно скрывать ссылки на следующую или предыдующую страницу соответственно. Ещё можно добавить какой-нибудь прикольный эффект при перелистывании, как это сделано у меня.

Самое приятное, что это решение полностью совместимо со старыми браузерами и не содержит никаких костылей. Кроме того, его очень легко прикрутить к уже существующим сайтам. Надо только научить сервер отдавать описанный выше JSON-объект.

В <div class="page-links"></div> можно добавить ссылки с номерами страниц, для перехода на конкретную страницу. При этом модифицировать Javascript-код вообще не нужно.

Ну разве не красота?