При навигации между страницами браузер сохраняет всю историю о переходах в специальном стеке, который называется history stack.
И каждый раз, когда браузер загружает новую веб-страницу или переходит по ссылке на веб-странице, браузер по умолчанию создает новую запись в истории просмотров. В коде JavaScript историю можно получить через свойство
history объекта window
. Данное свойство представляет тип History.
Объект History для взаимодействия с историей просмотров предоставляет ряд методов и свойств:
Свойство length возвращает количество записей в истории просмотров
console.log("В истории ", history.length, " записей");
Свойство state возвращает текущую запись из истории просмотров. По умолчанию при загрузке первой страницы в браузере это свойство равно null
console.log(history.state);
Метод back() переходит к прошлой записи в истории просмотров, аналогично нажатию на кнопку Назад/Back в браузере
history.back(); // перемещение назад к прошлой странице
Метод forward() переходит к следующей просмотренной странице, аналогично нажатию на кнопку Вперед/Next в браузере
history.forward(); // перемещение вперед к следующей странице
Метод go() позволяет перемещаться вперед и назад по истории на определенное число страниц. Методу передается приращение, начиная с текущей веб-страницы. Например, значение -1 приводит к открытию предыдущей веб-страницы, а значение 1 вызывает открытие следующей веб-страницы. Если передается значение, для которого в истории нет соответствующей веб-страницы, этот метод ничего не делает. Если же метод вызывается без значения или со значением 0, текущая веб-страница перезагружается
history.go(-2); // переход на 2 страницы назад history.go(2); // переход на 2 страницы вперед history.go(0); // перезагружаем текущую страницу
Метод pushState() программно добавляет новую запись в историю просмотров. Он принимает три параметра:
history.pushState(state, title[, url])
Параметр state
представляет добавляемый объект в историю просмотров. В качестве такого объекта состояния может быть чем угодно
Параметр title
устанавливает заголовок. Стоит отметить, что браузеры могут игнорировать этот параметр
Параметр url
представляет URL-адрес новой записи в истории. Является необязательным. Однако если используется, этот адрес url в этом параметре
должен относиться к тому же домену, что и текущая страница. Браузер может устанавливать этот адрес в качестве текущего.
Простейший пример
const state = { url: "/", title: "Home", decription: "Home Page" }; // history.pushState(state, state.title); // без url history.pushState(state, state.title, state.url); // с url console.log(state); // {url: "/", title: "Home", decription: "Home Page"}
Метод replaceState() программно заменяет текущую запись в истории просмотров на новую. Он принимает те же три параметра:
history.replaceState(state, title, [url])
Простейший пример
const state = { url: "home", title: "Home", decription: "Home Page" }; history.replaceState(state, state.title, state.url);
Каждый раз, когда текущая запись в истории посещений меняется (например, при нажатии на кнопку "Назад" в браузере), срабатывает событие popstate. Соответственно если мы хотим обрабатывать перемещение по истории просмотров с помощью кнопок браузера Назад/Вперед, то нам надо обрабатывать данное событие.
Для обработки события popstate в обработчик события передается объект события типа PopStateEvent. В этом
объекте свойство state
указывает на запись, удаленную из истории просмотров:
window.addEventListener("popstate", (event) => { console.log(event.state); // получаем старое состояние });
В качестве примера применения History API определим простейший одностраничный сайт в виде следующей веб-страницы index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <nav><a href="#home">Home</a> | <a href="#about">About</a> | <a href="#contacts">Contacts</a></nav> <h1 id="content"></h1> <script> // Контейнер, в который загружаем контент const contentElement = document.getElementById("content"); // Объект, который содержит содержимое для различных страниц const pages = { home: { content: "Home Page", url: "#home"}, about: { content: "About Page", url: "#about" }, contacts: { content: "Contact Page", url: "#contacts"} }; // Обработчик нажатия на ссылки function handleClick(event){ // получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("#").pop(); // получаем страницу из объекта pages const page = pages[pageName]; // если текущий адрес совпадает с запрошенным, то игнорируем переход if(history.state.url != url) { contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок } return event.preventDefault(); } // устанавливаем обработчик для извлечения состояния в History API window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние }); // устанавливаем обработчик нажатия для кнопок const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); } // по умолчанию загружаем Home Page contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url); </script> </body> </html>
Итак, изначально на странице у нас три ссылки, нажимая на которые, мы будем переходить на условные страницы:
<nav><a href="#home">Home</a> | <a href="#about">About</a> | <a href="#contacts">Contacts</a></nav>
Для простоты предположим, что контекст условных страниц будет состоять из одного заголовка и будет загружаться в соответствующий элемент на странице:
<h1 id="content"></h1>
В коде JavaScript мы будем ссылаться на этот элемент через константу contentElement
В коде JavaScript определяем код условных страниц в виде объекта pages:
const pages = { home: { content: "Home Page", url: "#home"}, about: { content: "About Page", url: "#about" }, contacts: { content: "Contact Page", url: "#contacts"} };
Каждый объект однотипен: содержит свойство content, которое представляет содержимое условной страницы, и свойство url - адрес страницы. Собственно
состояние history.state
будет представлять один из этих объектов. Но тут важная условность -
для простоты названия этих страниц - home/about/constact совпадают с адресами ссылок. Можно было бы отвязать названия, но это привело бы к увеличению логики в
сугубо демонстрационном примере.
Для обработки нажатия ссылок определяется функция handleClick, в которую передается объект события. И из этого объекта события через
event.target
мы можем получить нажатую ссылку и ее данные. Так, в начале получаем адрес ссылки и название страницы (которое равно адресу без начального слеша):
// получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("#").pop(); // получаем страницу из объекта pages const page = pages[pageName];
Получив нужную страницу, смотрим, какая ссылка нажата. Например, мы не хотим, чтобы находясь на определенной странице, пользователь заново загружал данные этой страницы, повторно нажимая на одну и ту же ссылку. И для этой цели берем в истории просмотров текущее состояние и проверяем его свойство url. Если текущее состояние (по сути текущая страница) имеет тот же адрес url, который запрошен, то нет смысла заново перезагружать содержимое страницы:
if(history.state.url != url) {
Если запрошен адрес, отличный от текущего, то устанавливаем в качестве заголовка содержимое (свойство content) текущей страницы и добавляем запись в историю просмотров:
contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок
Поскольку браузеры могут не устанавливать автоматически заголовок, то устанавливаем его вручную с помощью свойства document.title
.
Таким образом, в истории просмотров появится запись о переходе по ссылке.
Следует отметить, что в реальном приложении, как правило, подобные условные страницы определяются в отдельных файлах и подгружаются через AJAX.
Для обработки переходов с помощью кнопок браузера Назад/Вперед устанавливаем обработчик для события popstate
:
window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние });
Здесь получаем извлеченное состояние из истории просмотров (event.state) и с помощью его свойства content устанавливаем содержимое заголовка.
В конце устанавливаем обработчик нажатия для кнопок:
const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); }
И по умолчанию устанавливаем в качестве текущей условной страницы объект home из объекта pages, добавляя при этом соответствующую запись в историю просмотров:
contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url);
Кинем веб-страницу в браузер и мы сможем переходить по ссылкам как по отдельным страницам:
Также вместо символов хеша # для опредения ссылки (то есть индентификаторов фрагмента) также можно использовать слеши /, что, к примеру, будет лучше для индексации страницы поисковиками. Так, пример выше мы можем переписать следующим образом:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <nav><a href="/home">Home</a> | <a href="/about">About</a> | <a href="/contacts">Contacts</a></nav> <h1 id="content"></h1> <script> // Контейнер, в который загружаем контент const contentElement = document.getElementById("content"); // Объект, который содержит содержимое для различных страниц const pages = { home: { content: "Home Page", url: "/home"}, about: { content: "About Page", url: "/about" }, contacts: { content: "Contact Page", url: "/contacts"} }; // Обработчик нажатия на ссылки function handleClick(event){ // получаем адрес перехода const url = event.target.getAttribute("href"); // получаем имя страницы, которая совпадает с адресом перехода const pageName = url.split("/").pop(); // получаем страницу из объекта pages const page = pages[pageName]; // если текущий адрес совпадает с запрошенным, то игнорируем переход if(history.state.url != url) { contentElement.textContent = page.content; // добавляем в историю history.pushState(page, // объект state event.target.textContent, // Title event.target.href // URL ); document.title = event.target.textContent; // если браузер не устанавливает заголовок } return event.preventDefault(); } // устанавливаем обработчик для извлечения состояния в History API window.addEventListener("popstate", (event) => { if(event.state) // если есть состояние contentElement.textContent = event.state.content; // получаем старое состояние }); // устанавливаем обработчик нажатия для кнопок const links = document.getElementsByTagName("a"); for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", handleClick, true); } // по умолчанию загружаем Home Page contentElement.textContent = pages.home.content; history.pushState(pages.home, "Home", pages.home.url); </script> </body> </html>
Но в этом случае страница должна располагаться на веб-сервере: