Книга: JavaScript. Подробное руководство, 6-е издание

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

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>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>I'm thinking of a number...</h1>
<!-- визуальное представление чисел, которые еще не были исключены -->
<table><tr><tdx/td><tdx/td><tdprompt"x/label><input type="text">
</body></html>

Оглавление книги


Генерация: 7.022. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз