22.2. Управление историей посещений

Веб-броузеры запоминают, какие документы загружались в окно, и предоставляют кнопки Back и Forward, позволяющие перемещаться между этими документами. Эта модель хранения истории посещений в броузерах появилась еще в те дни, когда документы были статическими и все вычисления выполнялись на стороне сервера. В настоящее время веб-приложения часто загружают содержимое динамически и отображают новые состояния приложения без полной перезагрузки документа. Такие приложения должны предусматривать собственные механизмы управления историей посещений, если необходимо дать пользователю возможность использовать кнопки Back и Forward для перехода из одного состояния приложения в другое интуитивно понятным способом. Спецификация HTML5 определяет два механизма управления историей посещений.

Простейший способ работы с историей посещений связан с использованием свойства location.hash и события «hashchange». На момент написания этих строк данный способ был также наиболее широко реализованным: его поддержка в броузерах появилась еще до того, как он был стандартизован спецификацией HTML5. В большинстве броузеров (кроме старых версий IE) изменение свойства location.hash приводит к изменению URL, отображаемого в строке ввода адреса, и добавлению записи в историю посещений. Свойство hash определяет идентификатор фрагмента в URL и традиционно использовалось для перемещения к разделу документа с указанным идентификатором. Но свойство location.hash не обязательно должно определять идентификатор элемента: в него можно записать произвольную строку. Если состояние приложения можно представить в виде строки, эту строку можно будет использовать как идентификатор фрагмента.

Предусмотрев изменение значения свойства location.hash, вы даете пользователю возможность использовать кнопки Back и Forward для перемещения между состояниями приложения. Чтобы такие перемещения были возможны, приложение должно иметь способ определять момент изменения состояния, прочитать строку, хранящуюся в виде идентификатора фрагмента, и обновить себя в соответствии с требуемым состоянием. Согласно спецификации HTML5, при изменении идентификатора фрагмента броузер должен возбуждать событие «hashchange» в объекте Window. В броузерах, поддерживающих событие «hashchange», можно присвоить свойству window.onhashchange функцию обработчика, которая будет вызываться при каждом изменении идентификатора фрагмента документа, вызванном перемещением по истории посещений. При вызове эта функция-обработчик должна проанализировать значение location.hash и отобразить содержимое страницы, соответствующее выбранному состоянию.

Спецификация HTML5 также определяет другой, более сложный и более надежный способ управления историей посещений, основанный на использовании метода history.pushState() и события «popstate». При переходе в новое состояние вебприложение может вызвать метод history.pushState(), чтобы добавить это состояние в историю посещений. В первом аргументе методу передается объект, содержащий всю информацию, необходимую для восстановления текущего состояния приложения. Для этой цели подойдет любой объект, который можно преобразовать в строку вызовом метода JSON.stringify(), а также некоторые встроенные типы, такие как Date и RegExp (смотрите врезку ниже). Во втором аргументе передается необязательное заглавие (простая текстовая строка), которую броузер сможет использовать для идентификации сохраненного состояния в истории посещений (например, в меню кнопки Back). В третьем необязательном аргументе передается строка URL, которая будет отображаться как адрес текущего состояния. Относительные URL-адреса интерпретируются относительно текущего адреса документа и нередко определяют лишь часть URL, соответствующую идентификатору фрагмента, такую как #state. Связывание различных состояний приложения с собственными URL-адресами дает пользователю возможность делать закладки на внутренние состояния приложения, и если в строке URL будет указан достаточное количество информации, приложение сможет восстановить это состояние при запуске с помощью закладки.

Структурированные копии

Как отмечалось выше, метод pushState() принимает объект с информацией о состоянии и создает его частную копию. Это полная, глубокая копия объекта: при ее создании рекурсивно копируется содержимое всех вложенных объектов и массивов. В стандарте HTML5 такие копии называются структурированными копиями. Процедура создания структурированной копии напоминает передачу объекта функции JSON. stringif у() и обработку результата функцией JSON.parse() (раздел 6.9). Но в формат JSON можно преобразовать только простые значения JavaScript, а также объекты и массивы. Стандарт HTML5 требует, чтобы алгоритм создания структурированных копий поддерживал возможность создания копий объектов Date и RegExp, ImageData (полученных из элементов <canvas> - раздел 21.4.14) и FileList, File и Blob (описывается в разделе 22.6). Функции JavaScript и объекты ошибок явно исключены из списка объектов, поддерживаемых алгоритмом создания структурированных копий, также как и большинство объектов среды выполнения, таких как окна, документы, элемент и т. д.

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

***********************************************

В дополнение к методу pushState() объект History определяет метод replaceState(), который принимает те же аргументы, но не просто добавляет новую запись в историю посещений, а замещает текущую запись.

Когда пользователь перемещается по истории посещений с помощью кнопок Back и Forward, броузер возбуждает событие «popstate» в объекте Window. Объект, связанный с этим событием, имеет свойство с именем state, содержащее копию (еще одну структурированную копию) объекта с информацией о состоянии, переданного методу pushState().

В примере 22.3 демонстрируется простое веб-приложение - игра «Угадай число», изображенная на рис. 22.1, - в которой используются описанные приемы сохранения истории посещений, определяемые стандартом HTML5, с целью дать пользователю возможность «вернуться назад», чтобы пересмотреть или повторить попытку.

Когда эта книга готовилась к печати, в броузере Firefox 4 было внесено два изменения в прикладной интерфейс объекта History, которые могут быть заимствованы и другими броузерами. Во-первых, в Firefox 4 информация о текущем состоянии теперь доступна через свойство state самого объекта History, а это означает, что вновь загружаемые страницы не должны ждать события «popstate». Во-вторых, Firefox 4 более не возбуждает событие «popstate» для вновь загруженных страниц, для которых отсутствует сохраненное состояние. Это второе изменение означает, например, что пример, приведенный ниже, будет работать неправильно в Firefox 4.

Примерх 22.3. Управление историей посещений с помощью pushStatef()

<!DOCTYPE html>

<html><head><title>I'm thinking of a number...</title>

<script>

window.onload = newgame;       // Начать новую игру при загрузке

window.onpopstate = popstate;  // Обработчик событий истории посещений

var state, ui;           // Глобальные переменные, инициализируемые в функции newgame()

function newgame(playagain) {  // Начинает новую игру "Угадай число"

  // Настроить объект для хранения необходимых элементов документа

  ui = {

    heading: null, // Заголовок <h1> в начале документа.

    prompt: null,  // Текст предложения ввести число.

    input: null,   // Поле, куда пользователь вводит-числа.

    low: null,     // Три ячейки таблицы для визуального представления

    mid: null,     // ...диапазона, в котором находится загаданное число.

    high: null

  };

  // Отыскать каждый из этих элементов по их атрибутам id

  for(var id in ui) ui[id] = document.getElementByld(id):

  // Определить обработчик событий для поля ввода

  ui.input.onchange = handleGuess;

  // Выбрать случайное число и инициализировать состояние игры

  state = {

    n: Math.floor(99 * Math.random()) + 1, // Целое число: 0 < n < 100

    low: 0, // Нижняя граница, где находится угадываемое число

    high: 100, // Верхняя граница, где находится угадываемое число

    guessnum: 0, // Количество выполненных попыток угадать число

    guess: undefined // Последнее число, указанное пользователем

  };

  // Изменить содержимое документа, чтобы отобразить начальное состояние

  display(state):

  // Эта функция вызывается как обработчик события onload, а также как обработчик щелчка

  // на кнопке Play Again (Играть еще раз), которая появляется в конце игры.

  // Во втором случае аргумент playagain будет иметь значение true, и если это так,

  // мы сохраняем новое состояние игры. Но если функция была вызвана в ответ

  // на событие "load", сохранять состояние не требуется, потому что событие "load"

  // может возникнуть также при переходе назад по истории посещений из какого-то

  // другого документа в существующее состояние игры. Если бы мы сохраняли начальное

  // состояние, в этом случае мы могли бы затереть имеющееся в истории актуальное

  // состояние игры. В броузерах, поддерживающих метод pushState(), за событием "load"

  // всегда следует событие "popstate". Поэтому, вместо того чтобы сохранять

  // состояние здесь, мы ждем событие "popstate". Если вместе с ним будет получен

  // объект состояния, мы просто используем его. Иначе, если событие "popstate"

  // содержит в поле state значение null, мы знаем, что была начата новая игра,

  // и поэтому используем replaceState для сохранения нового состояния игры,

  if (playagain === true) save(state);

}

// Сохраняет состояние игры с помощью метода pushStateO, если поддерживается

function save(state) {

  if (!history.pushState) return;// Вернуться, если pushState() не определен

  // С каждым состоянием мы связываем определенную строку URL-адреса.

  // Этот адрес отображает число попыток, но не содержит информации о состоянии игры,

  // поэтому его нельзя использовать для создания закладок.

  // Мы не можем поместить информацию о состоянии в URL-адрес,

  // потому что при этом пришлось бы указать в нем угадываемое число,

  var url = "#guess" + state.guessnum;

  // Сохранить объект с информацией о состоянии и строку URL

  history.pushState(state, // Сохраняемый объект с информацией о состоянии

                  "", // Заглавие: текущие броузеры игнорируют его

                  url); // URL состояния: бесполезен для закладок

}

// Обработчик события onpopstate, восстанавливающий состояние приложения,

function popState(event) {

  if (event.state) { // Если имеется объект состояния, восстановить его

    // Обратите внимание, что event.state является полной копией

    // сохраненного объекта состояния, поэтому мы можем изменять его,

    // не опасаясь изменить сохраненное значение.

    state = event.state; // Восстановить состояние

    displayCstate); // Отобразить восстановленное состояние

  }

  else {

    // Когда страница загружается впервые, событие "popstate" поступает

    // без объекта состояния. Заменить значение null действительным

    // состоянием: смотрите комментарий в функции newgame().

    // Нет необходимости вызывать display() здесь.

    history.replaceState(state, "", "#guess" + state.guessnum);

  }

};

// Этот обработчик событий вызывается всякий раз, когда пользователь вводит число.

// Он обновляет состояние игры, сохраняет и отображает его.

function handle6uess() {

  // Извлечь число из поля ввода

  var g = parseInt(this.value);

  // Если это число и оно попадает в требуемый диапазон

  if ((g > state.low) && (g < state.high)) {

    // Обновить объект состояния для этой попытки

    if (g < state.n)

      state.low = g;

    else

      if (g > state.n) state.high = g;

    state.guess = g;

    state.guessnum++;

    // Сохранить новое состояние в истории посещений

    save(state);

    // Изменить документ в ответ на попытку пользователя

    display(state);

  }

  else { // Ошибочная попытка: не сохранять новое состояние

    alert("Please enter a number greater than " + state.low +

          " and less than " + state.high);

  }

}

// Изменяет документ, отображая текущее состояние игры,

function display(state) {

  // Отобразить заголовок документа

  ui.heading.innerHTML = document.title =

     "I'm thinking of a number between " + state.low +

     " and " + state.high + "."

  // Отобразить диапазон чисел с помощью таблицы

  ui.low.style.width = state.low + "%";

  ui.mid.style.width = (state.high-state.low) + "%";

  ui.high.style.width = (100-state.high) + "%";

  // Сделать поле ввода видимым, очистить его и установить фокус ввода

  ui.input.style.visibility = "visible";

  ui.input.value = "";

  ui.input.focus();

  // Вывести приглашение к вводу, опираясь на последнюю попытку

  if (state.guess === undefined)

    ui.prompt.innerHTML = "Type your guess and hit Enter:";

  else if (state.guess < state.n)

    ui.prompt.innerHTML = state.guess + " is too low. Guess again:";

  else if (state.guess > state.n)

    ui.prompt.innerHTML = state.guess + " is too high. Guess again:";

  else {

    // Если число угадано, скрыть поле ввода и отобразить кнопку

    // Play Again (Играть еще раз).

    ui.input.style.visibility = "hidden"; // Попыток больше не будет

    ui.heading.innerHTML = document.title = state.guess + " is correct!";

    ui.prompt.innerHTML =

      "You Win! <button onclick='newgame(true)'>Play Again</button>“;

  }

}

</script>

<style> /* CSS-стили, чтобы придать игре привлекательный внешний вид */

  #prompt { font-size: 16pt; }

  table { width: 90%; margin:10px; margin-left:5%; }

  #low, «high { background-color: lightgray; height: 1em; }

  #mid { background-color: green; }

</style>

</head>

<body><!-- Следующие элементы образуют пользовательский интерфейс игры -->

<!-- Заголовок игры и текстовое представление диапазона чисел -->

<h1 id="heading">I'm thinking of a number...</h1>

<!-- визуальное представление чисел, которые еще не были исключены -->

<table><tr><td id="low"x/td><td id="mid"x/td><td id="high”x/tdx/trx/table>

<!-- Поле ввода чисел -->

<label id="prompt"x/label><input id=”input" type="text">

</body></html>