window.history

History API позволяет работать с историей браузера в пределах одной сессии.

Время чтения: 6 мин

Кратко

Секция статьи "Кратко"

History API даёт доступ к управлению историей браузера в рамках текущей сессии. Браузер создаёт новую сессию, когда пользователь открывает новую вкладку или новое окно браузера.

С помощью History API можно переходить по истории вперёд, назад и управлять содержимым истории. Доступ к API осуществляется с помощью объекта window.history.

Основные методы:

  • back() перемещает пользователя по истории на страницу назад;
  • forward() перемещает пользователя по истории на страницу вперёд;
  • go() универсальный метод для перемещения по истории вперёд или назад;
  • pushState() добавляет новую запись в истории сессии;
  • replaceState() изменяет текущую запись в истории сессии.

Как пишется

Секция статьи "Как пишется"

Сгруппируем методы и свойства History API на две части:

  • используемые для перемещения по истории в текущей сессии.
  • используемые для управления историей.

Перемещение по истории браузера

Секция статьи "Перемещение по истории браузера"

Метод back() перемещает пользователя по истории назад. Это работает аналогично нажатию кнопки «Назад» в браузере.

Метод back() не принимает аргументов и не возвращает результата.

        
          
          window.history.back()
          window.history.back()

        
        
          
        
      

Чтобы переместиться по истории вперёд, используется метод forward(). Он работает аналогично кнопке «Вперёд» в браузере.

Метод forward() не принимает аргументов и не возвращает результата.

        
          
          window.history.forward()
          window.history.forward()

        
        
          
        
      

Метод go() является универсальным и позволяет переместиться по истории вперёд или назад относительно текущей страницы.

Метод принимает один аргумент – число, которое определяет, на сколько шагов по истории вперёд или назад нужно перейти. Отрицательное значение определяет сколько шагов назад, а положительные – вперёд.

        
          
          window.history.go(2)// переместит на две позиции вперёд по историиwindow.history.go(-1)// переместит назад на одну страницу
          window.history.go(2)
// переместит на две позиции вперёд по истории

window.history.go(-1)
// переместит назад на одну страницу

        
        
          
        
      

Нулевая позиция 0 означает текущую страницу и вызов с нулём обновляет текущую страницу.

        
          
          window.history.go(0)
          window.history.go(0)

        
        
          
        
      

Метод back() аналогичен вызову window.history.go(-1), а метод forward() — вызову window.history.go(1).

Свойство length хранит количество записей в истории браузера в текущей сессии, включая текущую страницу. То есть новая история всегда будет начинаться с 1.

length работает только для чтения, при изменении значения ничего не произойдёт.

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

Секция статьи "Управление историей"

Для создания новой записи в истории используется метод pushState(), а для модификации текущей записи – replaceState().

Оба метода похожи с точки зрения использования и оба принимают три аргумента:

  • объект состояния, в который можно добавить любые данные, необходимые для навигации;
  • неиспользуемый параметр, который существует по историческим причинам;
  • новый URL-адрес, этот параметр опциональный.

У передаваемых аргументов есть ограничения, о которых стоит помнить:

  • В первый аргумент-объект можно записать любой объект с любыми данными, главное чтобы объект был сериализуемым. Браузеры могут накладывать ограничения на размер такого объекта.
  • Второй аргумент игнорируется всеми браузерами кроме Safari. Во избежание ошибок рекомендуют передавать пустую строку.
  • Третий аргумент, URL-адрес, необязательный. Если этот аргумент не был задан, то будет использован текущий URL. Новый URL-адрес должен использовать тот же протокол, домен и порт, иначе будет выброшена ошибка. Если новая запись ведёт на новый относительный адрес, то можно не передавать адрес полностью вместе с доменом, а записать только относительную часть через слэш. Например, /profile.

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

Открыть демо в новой вкладке

Предположим, что мы зашли на главную страницу сайта с новой вкладки. С помощью кнопки добавим новую запись в историю:

        
          
          window.history.pushState(  {},  '',  'about.html')
          window.history.pushState(
  {},
  '',
  'about.html'
)

        
        
          
        
      

Адрес в браузерной сроке location.path изменился, а так же обновилось количество записей в истории history.length. Теперь если нажать кнопку перезагрузки страницы, то мы увидим новую страницу.

Теперь можем изменить текущую запись в истории, нажав на кнопку. Ниже приведён фрагмент код, который делает изменение истории:

        
          
          window.history.replaceState(  {},  '',  'index.html')
          window.history.replaceState(
  {},
  '',
  'index.html'
)

        
        
          
        
      

Адрес в браузерной строке снова изменился, но количество записей осталось прежним, так как мы ничего не добавили. Далее продолжаем путь на следующую страницу.

Страница цен является последним пунктом в примере, с него можно только вернуться по истории назад на предыдущую страницу.

        
          
          window.history.back()// Или можно сделать window.history.go(-1)
          window.history.back()
// Или можно сделать window.history.go(-1)

        
        
          
        
      

Помните, как на предыдущей странице видели страницу «О нас» по ссылке about.html? Мы же попали на главную страницу. Это произошло потому что мы изменили текущую запись в истории, изменив её URL-адрес. В результате, когда мы возвращаемся назад по истории, то используем изменённый URL.

Как понять

Секция статьи "Как понять"

Классическая навигация

Секция статьи "Классическая навигация"

Основным способом в вебе навигации являются ссылки <a>. С помощью ссылок страницы соединяются друг с другом.

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

Верхние пункты списка – это недавно посещённые страницы, а последний пункт — страница, с которой началась сессия.

Навигация в одностраничных приложениях

Секция статьи "Навигация в одностраничных приложениях"

С другой стороны, есть одностраничные приложения, которые работают без перезагрузки страницы. Все содержимое они отрисовывают с помощью JavaScript. Приложение может перерисовать всю страницу, но на истории браузера это никак не отобразится.

История в браузере будет выглядеть как единственный пункт:

Это ведёт к плохому пользовательскому опыту, потому что пользователю сложно понять по каким страницам он перемещался.

Отсутствие истории ломает нативное поведение браузерных кнопок вперёд и назад, что очень важно, так как эти кнопки часто привязаны к жестам или системным кнопкам в смартфонах.

Сначала эту проблему решали добавлением хэша в адрес сайта с помощью поля window.location.hash. Присвоив новое значение в location.hash, адресная строка сразу же обновлялась и в историю добавлялась новая запись. Но с помощью хэша нельзя строить читаемые многоуровневые урлы, например https://example.com/category/cars/item/12345.

С появлением History API появилась возможность напрямую добавлять записи в историю просмотров. History API так же расширяет возможности для программного перемещения по истории браузера. Это позволяет создавать полноценную навигацию в одностраничных приложениях, менять адрес в браузерной строке и все это будет происходить без перезагрузки страницы.

На практике

Секция статьи "На практике"

Егор Огарков советует

Секция статьи "Егор Огарков советует"

🛠 Узнать, когда переход между страницами завершился

Секция статьи "🛠 Узнать, когда переход между страницами завершился"

Методы для перемещения по истории браузера back(), forward() и go() являются асинхронными, но они не принимают колбэков и не возвращают Promise.

Чтобы узнать, когда переход был завершён необходимо подписаться на событие popstate окна:

        
          
          window.addEventListener('popstate', (event) => {  console.log(`Перешли на адрес "${document.location}"`)})
          window.addEventListener('popstate', (event) => {
  console.log(`Перешли на адрес "${document.location}"`)
})

        
        
          
        
      

После этого можно добавить несколько записей в историю и сделать переходы. Если запустить скрипт на главной Доки — https://doka.guide/.

        
          
          history.pushState({}, '', '/js/')history.pushState({}, '', '/css/')history.back()// Напечатает: Перешли на адрес "https://doka.guide/js/"history.back()// Напечатает: Перешли на адрес "https://doka.guide/"history.go(2)// Напечатает: Перешли на адрес "https://doka.guide/css/"
          history.pushState({}, '', '/js/')
history.pushState({}, '', '/css/')

history.back()
// Напечатает: Перешли на адрес "https://doka.guide/js/"

history.back()
// Напечатает: Перешли на адрес "https://doka.guide/"

history.go(2)
// Напечатает: Перешли на адрес "https://doka.guide/css/"

        
        
          
        
      

Подписка на событие popstate используется в большинстве современных роутеров для фреймворков, чтобы давать разработчикам возможность запускать функции при переходе на новую страницу.

🛠 Передача состояния при навигации

Секция статьи "🛠 Передача состояния при навигации"

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

Сами передаваемые данные никак не влияют на URL-адрес или на сам переход. Данные будут скопированы в поле state в объекте события во время срабатывания события popstate.

        
          
          window.addEventListener('popstate', (event) => {  console.log(`Данные навигации: ${JSON.stringify(event.state)}`)});
          window.addEventListener('popstate', (event) => {
  console.log(`Данные навигации: ${JSON.stringify(event.state)}`)
});

        
        
          
        
      

Объект с данными может быть полезен, чтобы передавать информацию между переходами. Например, интернет-магазин, в котором есть фильтры товаров, может захотеть синхронизировать фильтры с URL-адресом, чтобы каждый пользователь мог поделиться ссылкой со всеми предустановленными фильтрами.

В приложения храним такие данные в виде простого объекта, а для URL будем преобразовывать его в строку параметров URL.

Рассмотрим пример базовой синхронизации параметров с урлом. Создадим функцию, которая будет превращать объект в строку URL-параметров. Например, объект { priceFrom: 200, priceTo:1000 } в ?priceFrom=200&priceTo=1000:

        
          
          function transformToURLParams(filters) {  const query = Object.entries(filters)    .map(([key, value]) => {      return `${key}=${value}`    })    .join('&')  return `?${query}`}
          function transformToURLParams(filters) {
  const query = Object.entries(filters)
    .map(([key, value]) => {
      return `${key}=${value}`
    })
    .join('&')

  return `?${query}`
}

        
        
          
        
      

Затем добавим функцию, которая будет обновлять историю и устанавливать выбранные фильтры в строку URL в браузере:

        
          
          function syncURL(filters) {  const path = document.location.pathname  const query = transformToURLParams(filters)  window.history.replaceState(filters, '', `${path}${query}`)}
          function syncURL(filters) {
  const path = document.location.pathname
  const query = transformToURLParams(filters)

  window.history.replaceState(filters, '', `${path}${query}`)
}

        
        
          
        
      

Теперь при каждом изменении фильтров достаточно вызвать функцию syncURL():

        
          
          syncURL({  priceFrom: 200,  priceTo: 1000,  manufacturer: 'Adidas'})
          syncURL({
  priceFrom: 200,
  priceTo: 1000,
  manufacturer: 'Adidas'
})

        
        
          
        
      

Фильтры могут работать и в обратную сторону — после загрузки страницы мы можем восстанавливать информацию о фильтрах из URL страницы.