/spa/webmaster/
Под словом "настоящий" понимается, что страниц как бы много, хотя на самом деле она одна. :)
Т.е. в браузере в адресной строке визуально меняется текущий адрес, без символа #, т.е. по-настоящему. В истории браузера добавляются страницы, по которым можно перемещаться кнопками вперёд-назад истории браузера.
Реализовать это можно благодаря методу
history.pushState(state, title [, url])
. Ну и важно продумать структуру сайта.Мне моя реализация нравится, что SEO-friendly. Контент показывается, если отключен Javascript.
Один скрипт отвечает за отдачу контента - index.php:
<?php // Определяю, есть ли страница для отображения $pagesArr = file("pages.txt"); $pageToShow = null; $requestUrl = substr($_SERVER['REQUEST_URI'], strlen("/spa/webmaster")); for ($i = 0; $i < count($pagesArr); $i ++) { $pageArr = explode("\t", $pagesArr[$i]); if ( $requestUrl === $pageArr[0]) { $pageToShow = $pageArr[0]; $title = trim($pageArr[2]); $pageFile = $pageArr[1]; break; } } if ($pageToShow === null) { header("HTTP/1.0 404 Not Found"); echo "404 Not Found"; exit; } echo " <!DOCTYPE html> <html> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> "; echo"<title>".$title."</title>"; echo " <link href=\"/spa/webmaster/spa-webmaster.css\" rel=\"stylesheet\" type=\"text/css\"> <link href=\"https://dmitriyzhuravlev.ru/style.css\" rel=\"stylesheet\" type=\"text/css\"> </head> <body> "; // Верх страницы require "header.html"; echo "<h1 class=\"spa\">".$title."</h1>"; echo "<div class=\"spa pageContent\">"; if (file_exists ( "pages/".$pageFile.".html" )) { require "pages/".$pageFile.".html"; } else { echo "Ошибка: для этой страницы не найдено файла с контентом."; } echo "</div>"; // Низ страницы require "bottom.html"; ?>Все запросы идут на него, поэтому такой .htaccess в каталоге /spa/webmaster/:
RewriteEngine On RewriteRule ^[a-zA-Z0-9-/]*$ index.phpОбратите внимание, что в regexp нет точки, поэтому запросы с точкой, т.е., например, к файлам *.png пройдут как обычные запросы к файлам.
Скрипт смотрит какую страницу запрашивают и сверяется с файлом pages.txt, где каждая строка - это одна возможная страница, её "технические" значения разделены табуляторами:
/ 000 Что это за SPA? /javascript/operator-precedence/ 001 Приоритет операторов в Javascript /javascript/navigator-mediadevices-getusermedia/ 002 Метод navigator.mediaDevices.getUserMedia() /javascript/truthy-falsy/ 003 Truthy и falsy значения /css/backdrop-filter/ 004 CSS-свойство backdrop-filterДля удобства всё хранится в текстовых файлах, БД не используется.
А Javascript делает POST-запросы к файлу getpagespa.php:
<?php // Этот PHP скрипт отдаёт JSON-объект с контентом запрошенной страницы после запроса методом POST. $postBody = file_get_contents('php://input'); if ($postBody === false) { exit; } $data = json_decode($postBody, true); // второй аргумент (true) означает, что надо вернуть массив (ассоциативный), а не объект if ( array_key_exists("page", $data) === false) { echoAnswer ("PHP скрипт: получен некорректный JSON-объект без ключа page", false); exit; } $pageFromRequest = $data["page"]; $pagesArr = file("pages.txt"); $pageToShow = null; for ($i = 0; $i < count($pagesArr); $i ++) { $pageArr = explode("\t", $pagesArr[$i]); if ( $pageFromRequest === $pageArr[0]) { $pageToShow = $pageArr[0]; $title = trim($pageArr[2]); $pageFile = $pageArr[1]; break; } } if ($pageToShow === null) { echoAnswer("Запрошенной страницы нет в pages.txt", false); exit; } if (file_exists ( "pages/".$pageFile.".html" )) { $content = file_get_contents("pages/".$pageFile.".html"); } else { echoAnswer("Запрошенная страница есть в pages.txt, но отстутствует файл ".$pageFile, false); exit; } echoAnswer($content, true); function echoAnswer($text, $allOk) { global $title, $pageFromRequest; $answer = [ "text" => $text, "allOk" => $allOk, "title" => $title, "page" => $pageFromRequest, ]; $text = json_encode( $answer ); echo $text; } ?>Ну, а это сам JS-код:
// Полифил метода closest // На самом деле он не нужен, ведь в этом коде клики в старых браузерах работают как клики по обычному сайту, а не SPA. window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(e){var t,o=(this.document||this.ownerDocument).querySelectorAll(e),n=this;do{for(t=o.length;--t>=0&&o.item(t)!==n;);}while(t<0&&(n=n.parentElement));return n}); document.addEventListener('click', handleSPAClick); window.addEventListener('popstate', handlePopstate); function handleSPAClick(e) { if ( !fetch ) { return; } // старые браузеры - обработка клика как по обычному сайту, а не SPA var linkElem = e.target.closest('a'); // ищу ссылку (тег a) по дереву вверх if ( linkElem ) { // определяю, какую страницу хочет открыть пользователь var pageForUser = null; var text = 'spa/webmaster'; var pos = linkElem.href.indexOf(text); if (pos !== - 1) { pageForUser = linkElem.href.substr(pos + text.length); } if (pageForUser !== null) { e.stopPropagation(); e.preventDefault(); loadPage(pageForUser); } } } function loadPage(page, doNotPushState) { // Функция подготавливает объект с контентом открываемой пользователем страницы // page - string - страница, которую надо открыть // doNotPushState - необязательный boolean - определяет нужно ли добавить в объект свойство, которое потом запрещает вызывать history.pushState в функции updateSPA // console.log('Пользователь хочет открыть страницу:'); // console.log(page); var bodyData = JSON.stringify( {page: page} ); // тело для POST запроса к PHP-скрипту // console.log('Запрашиваю через fetch контент.'); fetch('/spa/webmaster/getpagespa.php', { method: 'POST', headers: {'Content-Type': 'application/json;charset=utf-8'}, body: bodyData, }) .then(ifFulfilled) .catch(ifRejected); function ifRejected(e) { console.log('Ошибка при выполнении fetch.'); console.log(e); } function ifFulfilled(response) { // console.log('Ответ получен.'); if (response.ok) { response.text().then( useResponse ); } else { console.log('Проблема: response.ok равен ' + response.ok + ', а response.status равен ' + response.status); } } function useResponse(answer) { answer = JSON.parse(answer); // строка превращается в объект if (doNotPushState === true) { answer.doNotPushState = true; } updateSPA(answer); } } function updateSPA (data) { // Функция обновляет страницу: меняет содержимое тегов title, h1 и div с css-классом pageContent // После этого меняет историю браузера (если требуется). if (data.allOk !== true) { // Значение allOk заполняет PHP-скрипт console.log( 'Возникла какая-то проблема. В ответе у allOk не стоит значение true.' ); return; } document.title = data.title; document.querySelector('h1.spa').innerHTML = data.title; document.querySelector('.pageContent.spa').innerHTML = data.text; if (data.doNotPushState !== true) // Текущая функция запускается, если пользователь жмёт назад/вперёд в истории браузера, поэтому нельзя в таком случае добавлять в историю элемент. { data.doNotPushState = true; history.pushState( {spaPageData: data}, 'Title', '/spa/webmaster' + data.page); } } function handlePopstate(e) { // Функция сработает при использованнии Вперёд/Назад истории браузера. if (e.state && e.state.spaPageData) { // Используется старый объект state, "закешированный" браузером. // Из-за этого может показываться старый контент, если контент за время просмотра успел обновиться. // При желании можно исользовать код из блока else, который заново запрашивает содержимое страницы. updateSPA(e.state.spaPageData); } else { // Сюда попадёт пользователь, когда кнопкой назад вернётся на самую первую страницу сайта, загруженную без SPA-технологии // console.log('Нет event.state.'); var pageForUser = null; var text = 'spa/webmaster'; var pos = location.href.indexOf(text); if (pos !== - 1) { pageForUser = location.href.substr(pos + text.length); } if (pageForUser !== null) { loadPage(pageForUser, true); // второй параметр равный true означает, что не надо добавлять в историю браузера новый элемент } } }Ну и как обычно, в моём варианте SPA показывает всё нормально старым браузерам.
SPA-сайт - это, конечно, интересный вариант построения сайта со своим плюсами и минусами.
(из-за чертовых спамеров урлы в коментах теперь писать нельзя)
Комментариев нет